From 82908b1fb74c29a757319fa72b01b1a02a9b3a5d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 31 Jan 2023 07:42:10 +1000 Subject: [PATCH] [EC-826] Merge license sync feature branch to master (#2587) * [EC-634] Extract GenerateLicenseAsync to a query (#2373) * [EC-637] Add license sync to server (#2453) * [EC-1036] Show correct license sync date (#2626) * Update method name per new pattern --- src/Admin/Controllers/ToolsController.cs | 9 +- src/Api/Controllers/LicensesController.cs | 36 ++++-- .../OrganizationConnectionsController.cs | 2 +- .../OrganizationSponsorshipsController.cs | 1 + .../Controllers/OrganizationsController.cs | 9 +- ...elfHostedOrganizationLicensesController.cs | 117 ++++++++++++++++++ .../OrganizationConnectionRequestModel.cs | 3 +- src/Core/Entities/OrganizationConnection.cs | 33 ++++- ...lfHostedOrganizationLicenseRequestModel.cs | 7 ++ .../OrganizationConnectionData.cs | 3 +- .../BillingSyncConfig.cs | 15 ++- .../IConnectionConfig.cs | 6 + .../ScimConfig.cs | 14 ++- .../CreateOrganizationConnectionCommand.cs | 3 +- .../ICreateOrganizationConnectionCommand.cs | 3 +- .../IUpdateOrganizationConnectionCommand.cs | 3 +- .../IValidateBillingSyncKeyCommand.cs | 2 +- .../UpdateOrganizationConnectionCommand.cs | 3 +- .../ValidateBillingSyncKeyCommand.cs | 7 +- .../Cloud/CloudGetOrganizationLicenseQuery.cs | 38 ++++++ .../IGetOrganizationLicenseQuery.cs | 15 +++ .../SelfHostedGetOrganizationLicenseQuery.cs | 66 ++++++++++ ...OrganizationServiceCollectionExtensions.cs | 9 ++ .../SelfHostedSyncSponsorshipsCommand.cs | 15 +-- src/Core/Services/IOrganizationService.cs | 3 - .../Implementations/OrganizationService.cs | 24 ---- .../OrganizationConnectionsControllerTests.cs | 2 +- .../OrganizationsControllerTests.cs | 5 +- .../AutoFixture/SutProviderExtensions.cs | 50 ++++++++ .../SubscriptionInfoCustomization.cs | 18 +++ .../Entities/OrganizationConnectionTests.cs | 78 ++++++++++++ .../BillingSyncConfigTests.cs | 27 ++++ .../ScimConfigTests.cs | 23 ++++ .../ValidateBillingSyncKeyCommandTests.cs | 4 +- .../CloudGetOrganizationLicenseQueryTests.cs | 64 ++++++++++ ...fHostedGetOrganizationLicenseQueryTests.cs | 94 ++++++++++++++ .../SelfHostedSyncSponsorshipsCommandTests.cs | 58 ++------- 37 files changed, 746 insertions(+), 123 deletions(-) create mode 100644 src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs create mode 100644 src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs create mode 100644 src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs rename src/Core/OrganizationFeatures/{OrganizationSponsorships/FamiliesForEnterprise => OrganizationConnections}/Interfaces/IValidateBillingSyncKeyCommand.cs (64%) rename src/Core/OrganizationFeatures/{OrganizationSponsorships/FamiliesForEnterprise/Cloud => OrganizationConnections}/ValidateBillingSyncKeyCommand.cs (70%) create mode 100644 src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs create mode 100644 test/Common/AutoFixture/SutProviderExtensions.cs create mode 100644 test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs create mode 100644 test/Core.Test/Entities/OrganizationConnectionTests.cs create mode 100644 test/Core.Test/Models/OrganizationConnectionConfigs/BillingSyncConfigTests.cs create mode 100644 test/Core.Test/Models/OrganizationConnectionConfigs/ScimConfigTests.cs rename test/Core.Test/OrganizationFeatures/{OrganizationSponsorships/FamiliesForEnterprise/Cloud => OrganizationConnections}/ValidateBillingSyncKeyCommandTests.cs (91%) create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index 9bd6189b3e..b18864d371 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Bit.Admin.Models; using Bit.Core.Entities; using Bit.Core.Models.BitStripe; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -18,7 +19,7 @@ public class ToolsController : Controller { private readonly GlobalSettings _globalSettings; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationService _organizationService; + private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly IUserService _userService; private readonly ITransactionRepository _transactionRepository; private readonly IInstallationRepository _installationRepository; @@ -30,7 +31,7 @@ public class ToolsController : Controller public ToolsController( GlobalSettings globalSettings, IOrganizationRepository organizationRepository, - IOrganizationService organizationService, + ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, IUserService userService, ITransactionRepository transactionRepository, IInstallationRepository installationRepository, @@ -41,7 +42,7 @@ public class ToolsController : Controller { _globalSettings = globalSettings; _organizationRepository = organizationRepository; - _organizationService = organizationService; + _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; _userService = userService; _transactionRepository = transactionRepository; _installationRepository = installationRepository; @@ -259,7 +260,7 @@ public class ToolsController : Controller if (organization != null) { - var license = await _organizationService.GenerateLicenseAsync(organization, + var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, model.InstallationId.Value, model.Version); var ms = new MemoryStream(); await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented); diff --git a/src/Api/Controllers/LicensesController.cs b/src/Api/Controllers/LicensesController.cs index 63ed824795..f5391fddc2 100644 --- a/src/Api/Controllers/LicensesController.cs +++ b/src/Api/Controllers/LicensesController.cs @@ -1,6 +1,9 @@ using Bit.Core.Context; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.OrganizationLicenses; using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; @@ -14,26 +17,26 @@ namespace Bit.Api.Controllers; [SelfHosted(NotSelfHostedOnly = true)] public class LicensesController : Controller { - private readonly ILicensingService _licensingService; private readonly IUserRepository _userRepository; private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationService _organizationService; + private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; + private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; private readonly ICurrentContext _currentContext; public LicensesController( - ILicensingService licensingService, IUserRepository userRepository, IUserService userService, IOrganizationRepository organizationRepository, - IOrganizationService organizationService, + ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, + IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, ICurrentContext currentContext) { - _licensingService = licensingService; _userRepository = userRepository; _userService = userService; _organizationRepository = organizationRepository; - _organizationService = organizationService; + _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; + _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; _currentContext = currentContext; } @@ -55,21 +58,30 @@ public class LicensesController : Controller return license; } + /// + /// Used by self-hosted installations to get an updated license file + /// [HttpGet("organization/{id}")] - public async Task GetOrganization(string id, [FromQuery] string key) + public async Task OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model) { - var org = await _organizationRepository.GetByIdAsync(new Guid(id)); - if (org == null) + var organization = await _organizationRepository.GetByIdAsync(new Guid(id)); + if (organization == null) { - return null; + throw new NotFoundException("Organization not found."); } - else if (!org.LicenseKey.Equals(key)) + + if (!organization.LicenseKey.Equals(model.LicenseKey)) { await Task.Delay(2000); throw new BadRequestException("Invalid license key."); } - var license = await _organizationService.GenerateLicenseAsync(org, _currentContext.InstallationId.Value); + if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey)) + { + throw new BadRequestException("Invalid Billing Sync Key"); + } + + var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value); return license; } } diff --git a/src/Api/Controllers/OrganizationConnectionsController.cs b/src/Api/Controllers/OrganizationConnectionsController.cs index 13260b7cca..b7329a14fa 100644 --- a/src/Api/Controllers/OrganizationConnectionsController.cs +++ b/src/Api/Controllers/OrganizationConnectionsController.cs @@ -191,7 +191,7 @@ public class OrganizationConnectionsController : Controller Guid? organizationConnectionId, OrganizationConnectionRequestModel model, Func, Task> validateAction = null) - where T : new() + where T : IConnectionConfig { var typedModel = new OrganizationConnectionRequestModel(model); if (validateAction != null) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index fc5d38db1c..d9715d07c3 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -5,6 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index bb7c3a63a2..baf0dbfaaf 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -11,6 +11,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -37,6 +38,7 @@ public class OrganizationsController : Controller private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly GlobalSettings _globalSettings; public OrganizationsController( @@ -53,6 +55,7 @@ public class OrganizationsController : Controller IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand, ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand, IOrganizationApiKeyRepository organizationApiKeyRepository, + ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, GlobalSettings globalSettings) { _organizationRepository = organizationRepository; @@ -68,6 +71,7 @@ public class OrganizationsController : Controller _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand; _createOrganizationApiKeyCommand = createOrganizationApiKeyCommand; _organizationApiKeyRepository = organizationApiKeyRepository; + _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; _globalSettings = globalSettings; } @@ -149,7 +153,8 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var license = await _organizationService.GenerateLicenseAsync(orgIdGuid, installationId); + var org = await _organizationRepository.GetByIdAsync(new Guid(id)); + var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId); if (license == null) { throw new NotFoundException(); @@ -215,6 +220,7 @@ public class OrganizationsController : Controller return new OrganizationResponseModel(result.Item1); } + [Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")] [HttpPost("license")] [SelfHosted(SelfHostedOnly = true)] public async Task PostLicense(OrganizationCreateLicenseRequestModel model) @@ -448,6 +454,7 @@ public class OrganizationsController : Controller } } + [Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")] [HttpPost("{id}/license")] [SelfHosted(SelfHostedOnly = true)] public async Task PostLicense(string id, LicenseRequestModel model) diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs new file mode 100644 index 0000000000..92cf50ae41 --- /dev/null +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationLicensesController.cs @@ -0,0 +1,117 @@ +using Bit.Api.Models.Request; +using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response.Organizations; +using Bit.Api.Utilities; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers.SelfHosted; + +[Route("organizations/licenses/self-hosted")] +[Authorize("Application")] +[SelfHosted(SelfHostedOnly = true)] +public class SelfHostedOrganizationLicensesController : Controller +{ + private readonly ICurrentContext _currentContext; + private readonly ISelfHostedGetOrganizationLicenseQuery _selfHostedGetOrganizationLicenseQuery; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + private readonly IOrganizationService _organizationService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserService _userService; + + public SelfHostedOrganizationLicensesController( + ICurrentContext currentContext, + ISelfHostedGetOrganizationLicenseQuery selfHostedGetOrganizationLicenseQuery, + IOrganizationConnectionRepository organizationConnectionRepository, + IOrganizationService organizationService, + IOrganizationRepository organizationRepository, + IUserService userService) + { + _currentContext = currentContext; + _selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery; + _organizationConnectionRepository = organizationConnectionRepository; + _organizationService = organizationService; + _organizationRepository = organizationRepository; + _userService = userService; + } + + [HttpPost("")] + public async Task PostLicenseAsync(OrganizationCreateLicenseRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + if (license == null) + { + throw new BadRequestException("Invalid license"); + } + + var result = await _organizationService.SignUpAsync(license, user, model.Key, + model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); + return new OrganizationResponseModel(result.Item1); + } + + [HttpPost("{id}")] + public async Task PostLicenseAsync(string id, LicenseRequestModel model) + { + var orgIdGuid = new Guid(id); + if (!await _currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + var license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); + if (license == null) + { + throw new BadRequestException("Invalid license"); + } + + await _organizationService.UpdateLicenseAsync(new Guid(id), license); + } + + [HttpPost("{id}/sync")] + public async Task SyncLicenseAsync(string id) + { + var organization = await _organizationRepository.GetByIdAsync(new Guid(id)); + if (organization == null) + { + throw new NotFoundException(); + } + + if (!await _currentContext.OrganizationOwner(organization.Id)) + { + throw new NotFoundException(); + } + + var billingSyncConnection = + (await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id, + OrganizationConnectionType.CloudBillingSync)).FirstOrDefault(); + if (billingSyncConnection == null) + { + throw new NotFoundException("Unable to get Cloud Billing Sync connection"); + } + + var license = + await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(organization, billingSyncConnection); + + await _organizationService.UpdateLicenseAsync(organization.Id, license); + + var config = billingSyncConnection.GetConfig(); + config.LastLicenseSync = DateTime.Now; + billingSyncConnection.SetConfig(config); + await _organizationConnectionRepository.ReplaceAsync(billingSyncConnection); + } +} diff --git a/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs index 9dbc9ca0a0..96a444c27a 100644 --- a/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs @@ -2,6 +2,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Utilities; namespace Bit.Api.Models.Request.Organizations; @@ -17,7 +18,7 @@ public class OrganizationConnectionRequestModel } -public class OrganizationConnectionRequestModel : OrganizationConnectionRequestModel where T : new() +public class OrganizationConnectionRequestModel : OrganizationConnectionRequestModel where T : IConnectionConfig { public T ParsedConfig { get; private set; } diff --git a/src/Core/Entities/OrganizationConnection.cs b/src/Core/Entities/OrganizationConnection.cs index cc07177384..5b466fb4a6 100644 --- a/src/Core/Entities/OrganizationConnection.cs +++ b/src/Core/Entities/OrganizationConnection.cs @@ -1,15 +1,16 @@ using System.Text.Json; using Bit.Core.Enums; +using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Utilities; namespace Bit.Core.Entities; -public class OrganizationConnection : OrganizationConnection where T : new() +public class OrganizationConnection : OrganizationConnection where T : IConnectionConfig { public new T Config { get => base.GetConfig(); - set => base.SetConfig(value); + set => base.SetConfig(value); } } @@ -26,7 +27,7 @@ public class OrganizationConnection : ITableObject Id = CoreHelpers.GenerateComb(); } - public T GetConfig() where T : new() + public T GetConfig() where T : IConnectionConfig { try { @@ -38,8 +39,32 @@ public class OrganizationConnection : ITableObject } } - public void SetConfig(T config) where T : new() + public void SetConfig(T config) where T : IConnectionConfig { Config = JsonSerializer.Serialize(config); } + + public bool Validate(out string exception) where T : IConnectionConfig + { + if (!Enabled) + { + exception = $"Connection disabled for organization {OrganizationId}"; + return false; + } + + if (string.IsNullOrWhiteSpace(Config)) + { + exception = $"No saved Connection config for organization {OrganizationId}"; + return false; + } + + var config = GetConfig(); + if (config == null) + { + exception = $"Error parsing Connection config for organization {OrganizationId}"; + return false; + } + + return config.Validate(out exception); + } } diff --git a/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs new file mode 100644 index 0000000000..365d88877e --- /dev/null +++ b/src/Core/Models/Api/Request/OrganizationLicenses/SelfHostedOrganizationLicenseRequestModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Api.OrganizationLicenses; + +public class SelfHostedOrganizationLicenseRequestModel +{ + public string LicenseKey { get; set; } + public string BillingSyncKey { get; set; } +} diff --git a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs index 3a3edaed45..7a9aa77110 100644 --- a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs @@ -1,9 +1,10 @@ using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.OrganizationConnectionConfigs; namespace Bit.Core.Models.Data.Organizations.OrganizationConnections; -public class OrganizationConnectionData where T : new() +public class OrganizationConnectionData where T : IConnectionConfig { public Guid? Id { get; set; } public OrganizationConnectionType Type { get; set; } diff --git a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs index 204e165d05..07f07093d2 100644 --- a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs +++ b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs @@ -1,7 +1,20 @@ namespace Bit.Core.Models.OrganizationConnectionConfigs; -public class BillingSyncConfig +public class BillingSyncConfig : IConnectionConfig { public string BillingSyncKey { get; set; } public Guid CloudOrganizationId { get; set; } + public DateTime? LastLicenseSync { get; set; } + + public bool Validate(out string exception) + { + if (string.IsNullOrWhiteSpace(BillingSyncKey)) + { + exception = "Failed to get Billing Sync Key"; + return false; + } + + exception = ""; + return true; + } } diff --git a/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs new file mode 100644 index 0000000000..9b02c359e4 --- /dev/null +++ b/src/Core/Models/OrganizationConnectionConfigs/IConnectionConfig.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Models.OrganizationConnectionConfigs; + +public interface IConnectionConfig +{ + bool Validate(out string exception); +} diff --git a/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs index 63a1606cb2..8a4fcb4e8c 100644 --- a/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs +++ b/src/Core/Models/OrganizationConnectionConfigs/ScimConfig.cs @@ -3,9 +3,21 @@ using Bit.Core.Enums; namespace Bit.Core.Models.OrganizationConnectionConfigs; -public class ScimConfig +public class ScimConfig : IConnectionConfig { public bool Enabled { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ScimProviderType? ScimProvider { get; set; } + + public bool Validate(out string exception) + { + if (!Enabled) + { + exception = "Scim Config is disabled"; + return false; + } + + exception = ""; + return true; + } } diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs index e3f308bc57..6c019001c0 100644 --- a/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs @@ -1,5 +1,6 @@ using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Repositories; @@ -14,7 +15,7 @@ public class CreateOrganizationConnectionCommand : ICreateOrganizationConnection _organizationConnectionRepository = organizationConnectionRepository; } - public async Task CreateAsync(OrganizationConnectionData connectionData) where T : new() + public async Task CreateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig { return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity()); } diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs index b31920b10a..a97be09c3b 100644 --- a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs @@ -1,9 +1,10 @@ using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; public interface ICreateOrganizationConnectionCommand { - Task CreateAsync(OrganizationConnectionData connectionData) where T : new(); + Task CreateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig; } diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs index 742e89c970..b245b89693 100644 --- a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs @@ -1,9 +1,10 @@ using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; public interface IUpdateOrganizationConnectionCommand { - Task UpdateAsync(OrganizationConnectionData connectionData) where T : new(); + Task UpdateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig; } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs similarity index 64% rename from src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs rename to src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs index 53e926903f..9fb979548a 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IValidateBillingSyncKeyCommand.cs @@ -1,6 +1,6 @@ using Bit.Core.Entities; -namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; public interface IValidateBillingSyncKeyCommand { diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs index 0d872b6f1f..3e64fd47ab 100644 --- a/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Repositories; @@ -15,7 +16,7 @@ public class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnection _organizationConnectionRepository = organizationConnectionRepository; } - public async Task UpdateAsync(OrganizationConnectionData connectionData) where T : new() + public async Task UpdateAsync(OrganizationConnectionData connectionData) where T : IConnectionConfig { if (!connectionData.Id.HasValue) { diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs similarity index 70% rename from src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs rename to src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs index 19c4398a70..a764bbcf23 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommand.cs @@ -1,20 +1,17 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; -using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; using Bit.Core.Repositories; -namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +namespace Bit.Core.OrganizationFeatures.OrganizationConnections; public class ValidateBillingSyncKeyCommand : IValidateBillingSyncKeyCommand { - private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationApiKeyRepository _apiKeyRepository; public ValidateBillingSyncKeyCommand( - IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationApiKeyRepository organizationApiKeyRepository) { - _organizationSponsorshipRepository = organizationSponsorshipRepository; _apiKeyRepository = organizationApiKeyRepository; } diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs new file mode 100644 index 0000000000..ff8a6d34fb --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Cloud/CloudGetOrganizationLicenseQuery.cs @@ -0,0 +1,38 @@ +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.OrganizationFeatures.OrganizationLicenses; + +public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuery +{ + private readonly IInstallationRepository _installationRepository; + private readonly IPaymentService _paymentService; + private readonly ILicensingService _licensingService; + + public CloudGetOrganizationLicenseQuery( + IInstallationRepository installationRepository, + IPaymentService paymentService, + ILicensingService licensingService) + { + _installationRepository = installationRepository; + _paymentService = paymentService; + _licensingService = licensingService; + } + + public async Task GetLicenseAsync(Organization organization, Guid installationId, + int? version = null) + { + var installation = await _installationRepository.GetByIdAsync(installationId); + if (installation is not { Enabled: true }) + { + throw new BadRequestException("Invalid installation id"); + } + + var subInfo = await _paymentService.GetSubscriptionAsync(organization); + return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs new file mode 100644 index 0000000000..2c66833e63 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/Interfaces/IGetOrganizationLicenseQuery.cs @@ -0,0 +1,15 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Business; + +namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; + +public interface ICloudGetOrganizationLicenseQuery +{ + Task GetLicenseAsync(Organization organization, Guid installationId, + int? version = null); +} + +public interface ISelfHostedGetOrganizationLicenseQuery +{ + Task GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection); +} diff --git a/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs new file mode 100644 index 0000000000..84ae0a27db --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationLicenses/SelfHosted/SelfHostedGetOrganizationLicenseQuery.cs @@ -0,0 +1,66 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api.OrganizationLicenses; +using Bit.Core.Models.Business; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.Services; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.OrganizationFeatures.OrganizationLicenses; + +public class SelfHostedGetOrganizationLicenseQuery : BaseIdentityClientService, ISelfHostedGetOrganizationLicenseQuery +{ + private readonly IGlobalSettings _globalSettings; + + public SelfHostedGetOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger logger, ICurrentContext currentContext) + : base( + httpFactory, + globalSettings.Installation.ApiUri, + globalSettings.Installation.IdentityUri, + "api.licensing", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) + { + _globalSettings = globalSettings; + } + + public async Task GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection) + { + if (!_globalSettings.SelfHosted) + { + throw new BadRequestException("This action is only available for self-hosted."); + } + + if (!_globalSettings.EnableCloudCommunication) + { + throw new BadRequestException("Cloud communication is disabled in global settings"); + } + + if (!billingSyncConnection.Validate(out var exception)) + { + throw new BadRequestException(exception); + } + + var billingSyncConfig = billingSyncConnection.GetConfig(); + var cloudOrganizationId = billingSyncConfig.CloudOrganizationId; + + var response = await SendAsync( + HttpMethod.Get, $"licenses/organization/{cloudOrganizationId}", new SelfHostedOrganizationLicenseRequestModel() + { + BillingSyncKey = billingSyncConfig.BillingSyncKey, + LicenseKey = organization.LicenseKey, + }, true); + + if (response == null) + { + _logger.LogDebug("Organization License sync failed for '{OrgId}'", organization.Id); + throw new BadRequestException("An error has occurred. Check your internet connection and ensure the billing token is correct."); + } + + return response; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 18c2f44dc9..b8e2a775e2 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -7,6 +7,8 @@ using Bit.Core.OrganizationFeatures.OrganizationCollections; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationConnections; using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -32,6 +34,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationApiKeyCommandsQueries(); services.AddOrganizationCollectionCommands(); services.AddOrganizationGroupCommands(); + services.AddOrganizationLicenseCommandQueries(); } private static void AddOrganizationConnectionCommands(this IServiceCollection services) @@ -85,6 +88,12 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } + private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + private static void AddTokenizers(this IServiceCollection services) { services.AddSingleton>(serviceProvider => diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs index eed143838e..0d22b53bad 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs @@ -48,20 +48,13 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel { throw new BadRequestException("Failed to sync instance with cloud - Cloud communication is disabled in global settings"); } - if (!billingSyncConnection.Enabled) + + if (!billingSyncConnection.Validate(out var exception)) { - throw new BadRequestException($"Billing Sync Key disabled for organization {organizationId}"); - } - if (string.IsNullOrWhiteSpace(billingSyncConnection.Config)) - { - throw new BadRequestException($"No Billing Sync Key known for organization {organizationId}"); - } - var billingSyncConfig = billingSyncConnection.GetConfig(); - if (billingSyncConfig == null || string.IsNullOrWhiteSpace(billingSyncConfig.BillingSyncKey)) - { - throw new BadRequestException($"Failed to get Billing Sync Key for organization {organizationId}"); + throw new BadRequestException(exception); } + var billingSyncConfig = billingSyncConnection.GetConfig(); var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId)) .ToDictionary(i => i.SponsoringOrganizationUserId); if (!organizationSponsorshipsDict.Any()) diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index f3f3f16139..5383c1a3e3 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -56,9 +56,6 @@ public interface IOrganizationService IEnumerable organizationUserIds, Guid? deletingUserId); Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable groupIds, Guid? loggedInUserId); Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId); - Task GenerateLicenseAsync(Guid organizationId, Guid installationId); - Task GenerateLicenseAsync(Organization organization, Guid installationId, - int? version = null); Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index bff3b9fa4a..8f89e7ed6e 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1926,30 +1926,6 @@ public class OrganizationService : IOrganizationService EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw); } - public async Task GenerateLicenseAsync(Guid organizationId, Guid installationId) - { - var organization = await GetOrgById(organizationId); - return await GenerateLicenseAsync(organization, installationId); - } - - public async Task GenerateLicenseAsync(Organization organization, Guid installationId, - int? version = null) - { - if (organization == null) - { - throw new NotFoundException(); - } - - var installation = await _installationRepository.GetByIdAsync(installationId); - if (installation == null || !installation.Enabled) - { - throw new BadRequestException("Invalid installation id"); - } - - var subInfo = await _paymentService.GetSubscriptionAsync(organization); - return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version); - } - public async Task InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email, OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections, IEnumerable groups) diff --git a/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs b/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs index 594e708bdf..0f243b9efb 100644 --- a/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs @@ -366,7 +366,7 @@ public class OrganizationConnectionsControllerTests } private static OrganizationConnectionRequestModel RequestModelFromEntity(OrganizationConnection entity) - where T : new() + where T : IConnectionConfig { return new(new OrganizationConnectionRequestModel() { diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index fa1855c70d..f056beea80 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -29,6 +30,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery; private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly OrganizationsController _sut; @@ -48,12 +50,13 @@ public class OrganizationsControllerTests : IDisposable _rotateOrganizationApiKeyCommand = Substitute.For(); _organizationApiKeyRepository = Substitute.For(); _userService = Substitute.For(); + _cloudGetOrganizationLicenseQuery = Substitute.For(); _createOrganizationApiKeyCommand = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _organizationService, _userService, _paymentService, _currentContext, _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, - _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _globalSettings); + _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _cloudGetOrganizationLicenseQuery, _globalSettings); } public void Dispose() diff --git a/test/Common/AutoFixture/SutProviderExtensions.cs b/test/Common/AutoFixture/SutProviderExtensions.cs new file mode 100644 index 0000000000..1fdf226539 --- /dev/null +++ b/test/Common/AutoFixture/SutProviderExtensions.cs @@ -0,0 +1,50 @@ +using AutoFixture; +using Bit.Core.Services; +using Bit.Core.Settings; +using NSubstitute; +using RichardSzalay.MockHttp; + +namespace Bit.Test.Common.AutoFixture; + +public static class SutProviderExtensions +{ + public static SutProvider ConfigureBaseIdentityClientService(this SutProvider sutProvider, + string requestUrlFragment, HttpMethod requestHttpMethod, string identityResponse = null, string apiResponse = null) + where T : BaseIdentityClientService + { + var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties(); + fixture.AddMockHttp(); + + var settings = fixture.Create(); + settings.SelfHosted = true; + settings.EnableCloudCommunication = true; + + var apiUri = fixture.Create(); + var identityUri = fixture.Create(); + settings.Installation.ApiUri.Returns(apiUri.ToString()); + settings.Installation.IdentityUri.Returns(identityUri.ToString()); + + var apiHandler = new MockHttpMessageHandler(); + var identityHandler = new MockHttpMessageHandler(); + var syncUri = string.Concat(apiUri, requestUrlFragment); + var tokenUri = string.Concat(identityUri, "connect/token"); + + apiHandler.When(requestHttpMethod, syncUri) + .Respond("application/json", apiResponse); + identityHandler.When(HttpMethod.Post, tokenUri) + .Respond("application/json", identityResponse ?? "{\"access_token\":\"string\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"string\"}"); + + + var apiHttp = apiHandler.ToHttpClient(); + var identityHttp = identityHandler.ToHttpClient(); + + var mockHttpClientFactory = Substitute.For(); + mockHttpClientFactory.CreateClient(Arg.Is("client")).Returns(apiHttp); + mockHttpClientFactory.CreateClient(Arg.Is("identity")).Returns(identityHttp); + + return sutProvider + .SetDependency(settings) + .SetDependency(mockHttpClientFactory) + .Create(); + } +} diff --git a/test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs b/test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs new file mode 100644 index 0000000000..d16d13b578 --- /dev/null +++ b/test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs @@ -0,0 +1,18 @@ +using AutoFixture; +using Bit.Core.Models.Business; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.AutoFixture; + +public class SubscriptionInfoCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new SubscriptionInfoCustomization(); +} +public class SubscriptionInfoCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + // The Subscription property uses the external Stripe library, which Autofixture doesn't handle + fixture.Customize(c => c.Without(s => s.Subscription)); + } +} diff --git a/test/Core.Test/Entities/OrganizationConnectionTests.cs b/test/Core.Test/Entities/OrganizationConnectionTests.cs new file mode 100644 index 0000000000..32690416c2 --- /dev/null +++ b/test/Core.Test/Entities/OrganizationConnectionTests.cs @@ -0,0 +1,78 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Entities; + +public class OrganizationConnectionTests +{ + [Theory] + [BitAutoData] + public void OrganizationConnection_CanUse_Success(Guid connectionId, Guid organizationId) + { + var connection = new OrganizationConnection() + { + Id = connectionId, + OrganizationId = organizationId, + Enabled = true, + Type = OrganizationConnectionType.Scim, + Config = new ScimConfig() { Enabled = true } + }; + + Assert.True(connection.Validate(out var exception)); + Assert.True(string.IsNullOrEmpty(exception)); + } + + [Theory] + [BitAutoData] + public void OrganizationConnection_CanUse_WhenDisabled_ReturnsFalse(Guid connectionId, Guid organizationId) + { + + var connection = new OrganizationConnection() + { + Id = connectionId, + OrganizationId = organizationId, + Enabled = false, + Type = OrganizationConnectionType.Scim, + Config = new ScimConfig() { Enabled = true } + }; + + Assert.False(connection.Validate(out var exception)); + Assert.Contains("Connection disabled", exception); + } + + [Theory] + [BitAutoData] + public void OrganizationConnection_CanUse_WhenNoConfig_ReturnsFalse(Guid connectionId, Guid organizationId) + { + var connection = new OrganizationConnection() + { + Id = connectionId, + OrganizationId = organizationId, + Enabled = true, + Type = OrganizationConnectionType.Scim, + }; + + Assert.False(connection.Validate(out var exception)); + Assert.Contains("No saved Connection config", exception); + } + + [Theory] + [BitAutoData] + public void OrganizationConnection_CanUse_WhenConfigInvalid_ReturnsFalse(Guid connectionId, Guid organizationId) + { + var connection = new OrganizationConnection() + { + Id = connectionId, + OrganizationId = organizationId, + Enabled = true, + Type = OrganizationConnectionType.Scim, + Config = new ScimConfig() { Enabled = false } + }; + + Assert.False(connection.Validate(out var exception)); + Assert.Contains("Scim Config is disabled", exception); + } +} diff --git a/test/Core.Test/Models/OrganizationConnectionConfigs/BillingSyncConfigTests.cs b/test/Core.Test/Models/OrganizationConnectionConfigs/BillingSyncConfigTests.cs new file mode 100644 index 0000000000..05e717ebde --- /dev/null +++ b/test/Core.Test/Models/OrganizationConnectionConfigs/BillingSyncConfigTests.cs @@ -0,0 +1,27 @@ +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Models.OrganizationConnectionConfigs; + +public class BillingSyncConfigTests +{ + [Theory] + [BitAutoData] + public void BillingSyncConfig_CanUse_Success(string billingSyncKey) + { + var config = new BillingSyncConfig() { BillingSyncKey = billingSyncKey }; + + Assert.True(config.Validate(out var exception)); + Assert.True(string.IsNullOrEmpty(exception)); + } + + [Fact] + public void BillingSyncConfig_CanUse_WhenNoKey_ReturnsFalse() + { + var config = new BillingSyncConfig(); + + Assert.False(config.Validate(out var exception)); + Assert.Contains("Failed to get Billing Sync Key", exception); + } +} diff --git a/test/Core.Test/Models/OrganizationConnectionConfigs/ScimConfigTests.cs b/test/Core.Test/Models/OrganizationConnectionConfigs/ScimConfigTests.cs new file mode 100644 index 0000000000..fa476eea26 --- /dev/null +++ b/test/Core.Test/Models/OrganizationConnectionConfigs/ScimConfigTests.cs @@ -0,0 +1,23 @@ +using Bit.Core.Models.OrganizationConnectionConfigs; +using Xunit; + +namespace Bit.Core.Test.Models.OrganizationConnectionConfigs; + +public class ScimConfigTests +{ + [Fact] + public void ScimConfig_CanUse_Success() + { + var config = new ScimConfig() { Enabled = true }; + Assert.True(config.Validate(out var exception)); + Assert.True(string.IsNullOrEmpty(exception)); + } + + [Fact] + public void ScimConfig_CanUse_WhenDisabled_ReturnsFalse() + { + var config = new ScimConfig() { Enabled = false }; + Assert.False(config.Validate(out var exception)); + Assert.Contains("Config is disabled", exception); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommandTests.cs similarity index 91% rename from test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs rename to test/Core.Test/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommandTests.cs index 9b01e3035f..349f0316d7 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationConnections/ValidateBillingSyncKeyCommandTests.cs @@ -1,14 +1,14 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.OrganizationFeatures.OrganizationConnections; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections; [SutProviderCustomize] public class ValidateBillingSyncKeyCommandTests diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs new file mode 100644 index 0000000000..cb896ed717 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -0,0 +1,64 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; + +[SubscriptionInfoCustomize] +[OrganizationLicenseCustomize] +[SutProviderCustomize] +public class CloudGetOrganizationLicenseQueryTests +{ + [Theory] + [BitAutoData] + public async Task GetLicenseAsync_InvalidInstallationId_Throws(SutProvider sutProvider, + Organization organization, Guid installationId, int version) + { + sutProvider.GetDependency().GetByIdAsync(installationId).ReturnsNull(); + var exception = await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId, version)); + Assert.Contains("Invalid installation id", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetLicenseAsync_DisabledOrganization_Throws(SutProvider sutProvider, + Organization organization, Guid installationId, Installation installation) + { + installation.Enabled = false; + sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); + + var exception = await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId)); + Assert.Contains("Invalid installation id", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetLicenseAsync_CreatesAndReturns(SutProvider sutProvider, + Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo, + byte[] licenseSignature) + { + installation.Enabled = true; + sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); + sutProvider.GetDependency().GetSubscriptionAsync(organization).Returns(subInfo); + sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); + + var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId); + + Assert.Equal(LicenseType.Organization, result.LicenseType); + Assert.Equal(organization.Id, result.Id); + Assert.Equal(installationId, result.InstallationId); + Assert.Equal(licenseSignature, result.SignatureBytes); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs new file mode 100644 index 0000000000..df4a93305c --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/SelfHostedGetOrganizationLicenseQueryTests.cs @@ -0,0 +1,94 @@ +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationLicenses; +using Bit.Core.Settings; +using Bit.Core.Test.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses; + +[SutProviderCustomize] +public class SelfHostedGetOrganizationLicenseQueryTests +{ + private static SutProvider GetSutProvider(BillingSyncConfig config, + string apiResponse = null) + { + return new SutProvider() + .ConfigureBaseIdentityClientService($"licenses/organization/{config.CloudOrganizationId}", + HttpMethod.Get, apiResponse: apiResponse); + } + + [Theory] + [BitAutoData] + [OrganizationLicenseCustomize] + public async void GetLicenseAsync_Success(Organization organization, + OrganizationConnection billingSyncConnection, BillingSyncConfig config, OrganizationLicense license) + { + var sutProvider = GetSutProvider(config, JsonSerializer.Serialize(license)); + billingSyncConnection.Enabled = true; + billingSyncConnection.Config = config; + + var result = await sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection); + AssertHelper.AssertPropertyEqual(result, license); + } + + [Theory] + [BitAutoData] + public async void GetLicenseAsync_WhenNotSelfHosted_Throws(Organization organization, + OrganizationConnection billingSyncConnection, BillingSyncConfig config) + { + var sutProvider = GetSutProvider(config); + sutProvider.GetDependency().SelfHosted = false; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection)); + Assert.Contains("only available for self-hosted", exception.Message); + } + + [Theory] + [BitAutoData] + public async void GetLicenseAsync_WhenCloudCommunicationDisabled_Throws(Organization organization, + OrganizationConnection billingSyncConnection, BillingSyncConfig config) + { + var sutProvider = GetSutProvider(config); + sutProvider.GetDependency().EnableCloudCommunication = false; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection)); + Assert.Contains("Cloud communication is disabled", exception.Message); + } + + [Theory] + [BitAutoData] + public async void GetLicenseAsync_WhenCantUseConnection_Throws(Organization organization, + OrganizationConnection billingSyncConnection, BillingSyncConfig config) + { + var sutProvider = GetSutProvider(config); + billingSyncConnection.Enabled = false; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection)); + Assert.Contains("Connection disabled", exception.Message); + } + + [Theory] + [BitAutoData] + public async void GetLicenseAsync_WhenNullResponse_Throws(Organization organization, + OrganizationConnection billingSyncConnection, BillingSyncConfig config) + { + var sutProvider = GetSutProvider(config); + billingSyncConnection.Enabled = true; + billingSyncConnection.Config = config; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection)); + Assert.Contains("An error has occurred. Check your internet connection and ensure the billing token is correct.", + exception.Message); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs index 5ec93a976b..1cfe38bf1d 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using AutoFixture; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Response.OrganizationSponsorships; @@ -12,55 +11,22 @@ using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; -using RichardSzalay.MockHttp; using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTestsBase { - - public static SutProvider GetSutProvider(bool enableCloudCommunication = true, string identityResponse = null, string apiResponse = null) + private static SutProvider GetSutProvider(string apiResponse = null) { - var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties(); - fixture.AddMockHttp(); - - var settings = fixture.Create(); - settings.SelfHosted = true; - settings.EnableCloudCommunication = enableCloudCommunication; - - var apiUri = fixture.Create(); - var identityUri = fixture.Create(); - settings.Installation.ApiUri.Returns(apiUri.ToString()); - settings.Installation.IdentityUri.Returns(identityUri.ToString()); - - var apiHandler = new MockHttpMessageHandler(); - var identityHandler = new MockHttpMessageHandler(); - var syncUri = string.Concat(apiUri, "organization/sponsorship/sync"); - var tokenUri = string.Concat(identityUri, "connect/token"); - - apiHandler.When(HttpMethod.Post, syncUri) - .Respond("application/json", apiResponse); - identityHandler.When(HttpMethod.Post, tokenUri) - .Respond("application/json", identityResponse ?? "{\"access_token\":\"string\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"string\"}"); - - - var apiHttp = apiHandler.ToHttpClient(); - var identityHttp = identityHandler.ToHttpClient(); - - var mockHttpClientFactory = Substitute.For(); - mockHttpClientFactory.CreateClient(Arg.Is("client")).Returns(apiHttp); - mockHttpClientFactory.CreateClient(Arg.Is("identity")).Returns(identityHttp); - - return new SutProvider(fixture) - .SetDependency(settings) - .SetDependency(mockHttpClientFactory) - .Create(); + return new SutProvider() + .ConfigureBaseIdentityClientService("organization/sponsorship/sync", + HttpMethod.Post, apiResponse: apiResponse); } [Theory] [BitAutoData] - public async Task SyncOrganization_BillingSyncKeyDisabled_ThrowsBadRequest( + public async Task SyncOrganization_BillingSyncConnectionDisabled_ThrowsBadRequest( Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) { var sutProvider = GetSutProvider(); @@ -73,7 +39,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); - Assert.Contains($"Billing Sync Key disabled", exception.Message); + Assert.Contains($"Connection disabled", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -85,7 +51,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests [Theory] [BitAutoData] - public async Task SyncOrganization_BillingSyncKeyEmpty_ThrowsBadRequest( + public async Task SyncOrganization_BillingSyncConfigEmpty_ThrowsBadRequest( Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) { var sutProvider = GetSutProvider(); @@ -94,7 +60,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); - Assert.Contains($"No Billing Sync Key known", exception.Message); + Assert.Contains($"No saved Connection config", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -109,7 +75,8 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests public async Task SyncOrganization_CloudCommunicationDisabled_EarlyReturn( Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) { - var sutProvider = GetSutProvider(false); + var sutProvider = GetSutProvider(); + sutProvider.GetDependency().EnableCloudCommunication = false; var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); @@ -136,7 +103,8 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o)) })); - var sutProvider = GetSutProvider(apiResponse: syncJsonResponse); + var sutProvider = GetSutProvider(syncJsonResponse); + billingSyncConnection.SetConfig(new BillingSyncConfig { BillingSyncKey = "okslkcslkjf" @@ -166,7 +134,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o) { CloudSponsorshipRemoved = true }) })); - var sutProvider = GetSutProvider(apiResponse: syncJsonResponse); + var sutProvider = GetSutProvider(syncJsonResponse); billingSyncConnection.SetConfig(new BillingSyncConfig { BillingSyncKey = "okslkcslkjf"