diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6ab8d44d6..e5c5d0e7a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,9 @@ on: types: [opened, synchronize] workflow_call: inputs: {} + +permissions: + contents: read env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" @@ -237,18 +240,10 @@ jobs: fi echo "tags=$TAGS" >> $GITHUB_OUTPUT - - name: Generate image full name - id: cache-name - env: - PROJECT_NAME: ${{ steps.setup.outputs.project_name }} - run: echo "name=${_AZ_REGISTRY}/${PROJECT_NAME}:buildcache" >> $GITHUB_OUTPUT - - name: Build Docker image id: build-artifacts uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0 with: - cache-from: type=registry,ref=${{ steps.cache-name.outputs.name }} - cache-to: type=registry,ref=${{ steps.cache-name.outputs.name}},mode=max context: . file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile platforms: | @@ -603,8 +598,9 @@ jobs: uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main with: project: server - pull_request_number: ${{ github.event.number }} + pull_request_number: ${{ github.event.number || 0 }} secrets: inherit + permissions: read-all check-failures: name: Check for failures diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 601989a473..0ee4aa53a9 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -99,7 +99,7 @@ services: - idp rabbitmq: - image: rabbitmq:management + image: rabbitmq:4.1.0-management container_name: rabbitmq ports: - "5672:5672" @@ -108,7 +108,7 @@ services: RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} volumes: - - rabbitmq_data:/var/lib/rabbitmq_data + - rabbitmq_data:/var/lib/rabbitmq profiles: - rabbitmq diff --git a/dev/setup_azurite.ps1 b/dev/setup_azurite.ps1 index ad9808f6c3..03b92d4465 100755 --- a/dev/setup_azurite.ps1 +++ b/dev/setup_azurite.ps1 @@ -11,7 +11,7 @@ $corsRules = (@{ AllowedMethods = @("Get", "PUT"); }); $containers = "attachments", "sendfiles", "misc"; -$queues = "event", "notifications", "reference-events", "mail"; +$queues = "event", "notifications", "mail"; $tables = "event", "metadata", "installationdevice"; # End configuration diff --git a/global.json b/global.json index d04c13bbb5..d25197db39 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ }, "msbuild-sdks": { "Microsoft.Build.Traversal": "4.1.0", - "Microsoft.Build.Sql": "0.1.9-preview" + "Microsoft.Build.Sql": "1.0.0" } } diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 37130d54ce..1309b2df6d 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -81,13 +81,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] UpdatePaymentMethodRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) @@ -111,13 +104,6 @@ public class ProviderBillingController( [FromRoute] Guid providerId, [FromBody] VerifyBankAccountRequestBody requestBody) { - var allowProviderPaymentMethod = featureService.IsEnabled(FeatureFlagKeys.PM18794_ProviderPaymentMethod); - - if (!allowProviderPaymentMethod) - { - return TypedResults.NotFound(); - } - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); if (provider == null) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 92b611f588..7b302f3724 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1064,7 +1064,7 @@ public class CiphersController : Controller [HttpPut("share")] [HttpPost("share")] - public async Task PutShareMany([FromBody] CipherBulkShareRequestModel model) + public async Task> PutShareMany([FromBody] CipherBulkShareRequestModel model) { var organizationId = new Guid(model.Ciphers.First().OrganizationId); if (!await _currentContext.OrganizationUser(organizationId)) @@ -1086,7 +1086,7 @@ public class CiphersController : Controller } } - var shareCiphers = new List<(Cipher, DateTime?)>(); + var shareCiphers = new List<(CipherDetails, DateTime?)>(); foreach (var cipher in model.Ciphers) { if (!ciphersDict.TryGetValue(cipher.Id.Value, out var existingCipher)) @@ -1096,7 +1096,7 @@ public class CiphersController : Controller ValidateClientVersionForFido2CredentialSupport(existingCipher); - shareCiphers.Add(((Cipher)existingCipher, cipher.LastKnownRevisionDate)); + shareCiphers.Add((cipher.ToCipherDetails(existingCipher), cipher.LastKnownRevisionDate)); } var updated = await _cipherService.ShareManyAsync( @@ -1106,7 +1106,8 @@ public class CiphersController : Controller userId ); - return updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, false)).ToArray(); + var response = updated.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp)); + return new ListResponseModel(response); } [HttpPost("purge")] diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 2fe1025ba7..d94c9a9a92 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,9 +1,7 @@ using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; -using Bit.Core; using Bit.Core.Services; -using Bit.Core.Utilities; using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; @@ -15,7 +13,6 @@ namespace Bit.Api.Vault.Controllers; [Route("tasks")] [Authorize("Application")] -[RequireFeature(FeatureFlagKeys.SecurityTasks)] public class SecurityTaskController : Controller { private readonly IUserService _userService; diff --git a/src/Core/AdminConsole/Enums/PolicyType.cs b/src/Core/AdminConsole/Enums/PolicyType.cs index 6f3bcd0102..f72637f862 100644 --- a/src/Core/AdminConsole/Enums/PolicyType.cs +++ b/src/Core/AdminConsole/Enums/PolicyType.cs @@ -17,6 +17,7 @@ public enum PolicyType : byte AutomaticAppLogIn = 12, FreeFamiliesSponsorshipPolicy = 13, RemoveUnlockWithPin = 14, + RestrictedItemTypesPolicy = 15, } public static class PolicyTypeExtensions @@ -43,7 +44,8 @@ public static class PolicyTypeExtensions PolicyType.ActivateAutofill => "Active auto-fill", PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications", PolicyType.FreeFamiliesSponsorshipPolicy => "Remove Free Bitwarden Families sponsorship", - PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN" + PolicyType.RemoveUnlockWithPin => "Remove unlock with PIN", + PolicyType.RestrictedItemTypesPolicy => "Restricted item types", }; } } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs index bd1f280cad..c94794765b 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs @@ -1,12 +1,15 @@ -using Bit.Core.Enums; +#nullable enable + +using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.Integrations; public interface IIntegrationMessage { IntegrationType IntegrationType { get; } - int RetryCount { get; set; } - DateTime? DelayUntilDate { get; set; } + string MessageId { get; set; } + int RetryCount { get; } + DateTime? DelayUntilDate { get; } void ApplyRetry(DateTime? handlerDelayUntilDate); string ToJson(); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs index d2f0bde693..ecf5d25c51 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public class IntegrationHandlerResult { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs index 1f288914d0..018d453cb9 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs @@ -1,13 +1,15 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.Enums; namespace Bit.Core.AdminConsole.Models.Data.Integrations; -public class IntegrationMessage : IIntegrationMessage +public class IntegrationMessage : IIntegrationMessage { public IntegrationType IntegrationType { get; set; } - public T Configuration { get; set; } - public string RenderedTemplate { get; set; } + public required string MessageId { get; set; } + public required string RenderedTemplate { get; set; } public int RetryCount { get; set; } = 0; public DateTime? DelayUntilDate { get; set; } @@ -22,12 +24,22 @@ public class IntegrationMessage : IIntegrationMessage DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); } - public string ToJson() + public virtual string ToJson() + { + return JsonSerializer.Serialize(this); + } +} + +public class IntegrationMessage : IntegrationMessage +{ + public required T Configuration { get; set; } + + public override string ToJson() { return JsonSerializer.Serialize(this); } - public static IntegrationMessage FromJson(string json) + public static IntegrationMessage? FromJson(string json) { return JsonSerializer.Deserialize>(json); } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs index 4fcce542ce..4f2c434ff6 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs index 2930004cbf..18b13248ec 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs index b81e50d403..a9b4150419 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs index e8217d3ad3..47e014ee2a 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs index e3e92c900f..c4c41db24f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs @@ -1,3 +1,5 @@ -namespace Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs index 59debed746..ede2123f7e 100644 --- a/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs +++ b/src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs @@ -1,4 +1,5 @@ - +#nullable enable + using System.Text.Json.Serialization; namespace Bit.Core.Models.Slack; diff --git a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs index 60b8789a6b..ec2db121db 100644 --- a/src/Core/AdminConsole/Services/EventLoggingListenerService.cs +++ b/src/Core/AdminConsole/Services/EventLoggingListenerService.cs @@ -1,13 +1,87 @@ -using Microsoft.Extensions.Hosting; +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Bit.Core.Services; public abstract class EventLoggingListenerService : BackgroundService { protected readonly IEventMessageHandler _handler; + protected ILogger _logger; - protected EventLoggingListenerService(IEventMessageHandler handler) + protected EventLoggingListenerService(IEventMessageHandler handler, ILogger logger) { - _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + _handler = handler; + _logger = logger; + } + + internal async Task ProcessReceivedMessageAsync(string body, string? messageId) + { + try + { + using var jsonDocument = JsonDocument.Parse(body); + var root = jsonDocument.RootElement; + + if (root.ValueKind == JsonValueKind.Array) + { + var eventMessages = root.Deserialize>(); + await _handler.HandleManyEventsAsync(eventMessages); + } + else if (root.ValueKind == JsonValueKind.Object) + { + var eventMessage = root.Deserialize(); + await _handler.HandleEventAsync(eventMessage); + } + else + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError("An error occurred while processing message: {MessageId} - Invalid JSON", messageId); + } + else + { + _logger.LogError("An Invalid JSON error occurred while processing a message with an empty message id"); + } + } + } + catch (JsonException exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId} - Invalid JSON", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An Invalid JSON error occurred while processing a message with an empty message id" + ); + } + } + catch (Exception exception) + { + if (!string.IsNullOrEmpty(messageId)) + { + _logger.LogError( + exception, + "An error occurred while processing message: {MessageId}", + messageId + ); + } + else + { + _logger.LogError( + exception, + "An error occurred while processing a message with an empty message id" + ); + } + } } } diff --git a/src/Core/AdminConsole/Services/IAzureServiceBusService.cs b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs new file mode 100644 index 0000000000..d254e763d5 --- /dev/null +++ b/src/Core/AdminConsole/Services/IAzureServiceBusService.cs @@ -0,0 +1,10 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IAzureServiceBusService : IEventIntegrationPublisher, IAsyncDisposable +{ + ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options); + Task PublishToRetryAsync(IIntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs similarity index 58% rename from src/Core/AdminConsole/Services/IIntegrationPublisher.cs rename to src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs index 986ea776e1..560da576b7 100644 --- a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs +++ b/src/Core/AdminConsole/Services/IEventIntegrationPublisher.cs @@ -2,7 +2,8 @@ namespace Bit.Core.Services; -public interface IIntegrationPublisher +public interface IEventIntegrationPublisher : IAsyncDisposable { Task PublishAsync(IIntegrationMessage message); + Task PublishEventAsync(string body); } diff --git a/src/Core/AdminConsole/Services/IRabbitMqService.cs b/src/Core/AdminConsole/Services/IRabbitMqService.cs new file mode 100644 index 0000000000..b0b9a72eac --- /dev/null +++ b/src/Core/AdminConsole/Services/IRabbitMqService.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public interface IRabbitMqService : IEventIntegrationPublisher +{ + Task CreateChannelAsync(CancellationToken cancellationToken = default); + Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default); + Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default); + Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task PublishToDeadLetterAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken); + Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs); +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 2ab10418a3..8b00204775 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -1,7 +1,7 @@ -using System.Text; -using System.Text.Json; +#nullable enable + +using System.Text; using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -9,67 +9,47 @@ namespace Bit.Core.Services; public class AzureServiceBusEventListenerService : EventLoggingListenerService { - private readonly ILogger _logger; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; public AzureServiceBusEventListenerService( IEventMessageHandler handler, - ILogger logger, + IAzureServiceBusService serviceBusService, + string subscriptionName, GlobalSettings globalSettings, - string subscriptionName) : base(handler) + ILogger logger) : base(handler, logger) { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = serviceBusService.CreateProcessor( + globalSettings.EventLogging.AzureServiceBus.EventTopicName, + subscriptionName, + new ServiceBusProcessorOptions()); _logger = logger; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - _processor.ProcessMessageAsync += async args => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(args.Message.Body)); - var root = jsonDocument.RootElement; - - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); - - } - await args.CompleteMessageAsync(args.Message); - } - catch (Exception exception) - { - _logger.LogError( - exception, - "An error occured while processing message: {MessageId}", - args.Message.MessageId - ); - } - }; - - _processor.ProcessErrorAsync += args => - { - _logger.LogError( - args.Exception, - "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", - args.EntityPath, - args.ErrorSource - ); - return Task.CompletedTask; - }; + _processor.ProcessMessageAsync += ProcessReceivedMessageAsync; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) + { + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + + private async Task ProcessReceivedMessageAsync(ProcessMessageEventArgs args) + { + await ProcessReceivedMessageAsync(Encoding.UTF8.GetString(args.Message.Body), args.Message.MessageId); + await args.CompleteMessageAsync(args.Message); + } + public override async Task StopAsync(CancellationToken cancellationToken) { await _processor.StopProcessingAsync(cancellationToken); @@ -79,7 +59,6 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService public override void Dispose() { _processor.DisposeAsync().GetAwaiter().GetResult(); - _client.DisposeAsync().GetAwaiter().GetResult(); base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs deleted file mode 100644 index 224f86a802..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Text.Json; -using Azure.Messaging.ServiceBus; -using Bit.Core.Models.Data; -using Bit.Core.Services; -using Bit.Core.Settings; - -namespace Bit.Core.AdminConsole.Services.Implementations; - -public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ServiceBusClient _client; - private readonly ServiceBusSender _sender; - - public AzureServiceBusEventWriteService(GlobalSettings globalSettings) - { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); - } - - public async Task CreateAsync(IEvent e) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(e)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var message = new ServiceBusMessage(JsonSerializer.SerializeToUtf8Bytes(events)) - { - ContentType = "application/json" - }; - - await _sender.SendMessageAsync(message); - } - - public async ValueTask DisposeAsync() - { - await _sender.DisposeAsync(); - await _client.DisposeAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs index 8244f39c09..55a39ec774 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -1,7 +1,6 @@ #nullable enable using Azure.Messaging.ServiceBus; -using Bit.Core.Settings; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -10,39 +9,30 @@ namespace Bit.Core.Services; public class AzureServiceBusIntegrationListenerService : BackgroundService { private readonly int _maxRetries; - private readonly string _subscriptionName; - private readonly string _topicName; + private readonly IAzureServiceBusService _serviceBusService; private readonly IIntegrationHandler _handler; - private readonly ServiceBusClient _client; private readonly ServiceBusProcessor _processor; - private readonly ServiceBusSender _sender; private readonly ILogger _logger; - public AzureServiceBusIntegrationListenerService( - IIntegrationHandler handler, + public AzureServiceBusIntegrationListenerService(IIntegrationHandler handler, + string topicName, string subscriptionName, - GlobalSettings globalSettings, + int maxRetries, + IAzureServiceBusService serviceBusService, ILogger logger) { _handler = handler; _logger = logger; - _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; - _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; - _subscriptionName = subscriptionName; + _maxRetries = maxRetries; + _serviceBusService = serviceBusService; - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); - _sender = _client.CreateSender(_topicName); + _processor = _serviceBusService.CreateProcessor(topicName, subscriptionName, new ServiceBusProcessorOptions()); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { _processor.ProcessMessageAsync += HandleMessageAsync; - _processor.ProcessErrorAsync += args => - { - _logger.LogError(args.Exception, "Azure Service Bus error"); - return Task.CompletedTask; - }; + _processor.ProcessErrorAsync += ProcessErrorAsync; await _processor.StartProcessingAsync(cancellationToken); } @@ -51,51 +41,67 @@ public class AzureServiceBusIntegrationListenerService : BackgroundService { await _processor.StopProcessingAsync(cancellationToken); await _processor.DisposeAsync(); - await _sender.DisposeAsync(); - await _client.DisposeAsync(); await base.StopAsync(cancellationToken); } - private async Task HandleMessageAsync(ProcessMessageEventArgs args) + internal Task ProcessErrorAsync(ProcessErrorEventArgs args) { - var json = args.Message.Body.ToString(); + _logger.LogError( + args.Exception, + "An error occurred. Entity Path: {EntityPath}, Error Source: {ErrorSource}", + args.EntityPath, + args.ErrorSource + ); + return Task.CompletedTask; + } + internal async Task HandleMessageAsync(string body) + { try { - var result = await _handler.HandleAsync(json); + var result = await _handler.HandleAsync(body); var message = result.Message; if (result.Success) { - await args.CompleteMessageAsync(args.Message); - return; + // Successful integration. Return true to indicate the message has been handled + return true; } message.ApplyRetry(result.DelayUntilDate); if (result.Retryable && message.RetryCount < _maxRetries) { - var scheduledTime = (DateTime)message.DelayUntilDate!; - var retryMsg = new ServiceBusMessage(message.ToJson()) - { - Subject = args.Message.Subject, - ScheduledEnqueueTime = scheduledTime - }; - - await _sender.SendMessageAsync(retryMsg); + // Publish message to the retry queue. It will be re-published for retry after a delay + // Return true to indicate the message has been handled + await _serviceBusService.PublishToRetryAsync(message); + return true; } else { - await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); - return; + // Non-recoverable failure or exceeded the max number of retries + // Return false to indicate this message should be dead-lettered + return false; } - - await args.CompleteMessageAsync(args.Message); } catch (Exception ex) { + // Unknown exception - log error, return true so the message will be acknowledged and not resent _logger.LogError(ex, "Unhandled error processing ASB message"); + return true; + } + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + if (await HandleMessageAsync(json)) + { await args.CompleteMessageAsync(args.Message); } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + } } } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs deleted file mode 100644 index 4a906e719f..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Azure.Messaging.ServiceBus; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Settings; - -namespace Bit.Core.Services; - -public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable -{ - private readonly ServiceBusClient _client; - private readonly ServiceBusSender _sender; - - public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings) - { - _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); - } - - public async Task PublishAsync(IIntegrationMessage message) - { - var json = message.ToJson(); - - var serviceBusMessage = new ServiceBusMessage(json) - { - Subject = message.IntegrationType.ToRoutingKey(), - }; - - await _sender.SendMessageAsync(serviceBusMessage); - } - - public async ValueTask DisposeAsync() - { - await _sender.DisposeAsync(); - await _client.DisposeAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs new file mode 100644 index 0000000000..7d24095819 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusService.cs @@ -0,0 +1,70 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class AzureServiceBusService : IAzureServiceBusService +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _eventSender; + private readonly ServiceBusSender _integrationSender; + + public AzureServiceBusService(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _eventSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); + _integrationSender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options) + { + return _client.CreateProcessor(topicName, subscriptionName, options); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishToRetryAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow, + MessageId = message.MessageId + }; + + await _integrationSender.SendMessageAsync(serviceBusMessage); + } + + public async Task PublishEventAsync(string body) + { + var message = new ServiceBusMessage(body) + { + ContentType = "application/json", + MessageId = Guid.NewGuid().ToString() + }; + + await _eventSender.SendMessageAsync(message); + } + + public async ValueTask DisposeAsync() + { + await _eventSender.DisposeAsync(); + await _integrationSender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs index aa545913b1..578dde9485 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureTableStorageEventHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs new file mode 100644 index 0000000000..519f8aeb32 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationEventWriteService.cs @@ -0,0 +1,32 @@ +#nullable enable + +using System.Text.Json; +using Bit.Core.Models.Data; + +namespace Bit.Core.Services; +public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDisposable +{ + private readonly IEventIntegrationPublisher _eventIntegrationPublisher; + + public EventIntegrationEventWriteService(IEventIntegrationPublisher eventIntegrationPublisher) + { + _eventIntegrationPublisher = eventIntegrationPublisher; + } + + public async Task CreateAsync(IEvent e) + { + var body = JsonSerializer.Serialize(e); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async Task CreateManyAsync(IEnumerable events) + { + var body = JsonSerializer.Serialize(events); + await _eventIntegrationPublisher.PublishEventAsync(body: body); + } + + public async ValueTask DisposeAsync() + { + await _eventIntegrationPublisher.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs index 9a80ed67b2..aa76fdf8bc 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Text.Json; +#nullable enable + +using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; @@ -7,11 +9,9 @@ using Bit.Core.Repositories; namespace Bit.Core.Services; -#nullable enable - public class EventIntegrationHandler( IntegrationType integrationType, - IIntegrationPublisher integrationPublisher, + IEventIntegrationPublisher eventIntegrationPublisher, IOrganizationIntegrationConfigurationRepository configurationRepository, IUserRepository userRepository, IOrganizationRepository organizationRepository) @@ -34,6 +34,7 @@ public class EventIntegrationHandler( var template = configuration.Template ?? string.Empty; var context = await BuildContextAsync(eventMessage, template); var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid(); var config = configuration.MergedConfiguration.Deserialize() ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); @@ -41,13 +42,14 @@ public class EventIntegrationHandler( var message = new IntegrationMessage { IntegrationType = integrationType, + MessageId = messageId.ToString(), Configuration = config, RenderedTemplate = renderedTemplate, RetryCount = 0, DelayUntilDate = null }; - await integrationPublisher.PublishAsync(message); + await eventIntegrationPublisher.PublishAsync(message); } } diff --git a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs index ee3a2d5db2..0fab787589 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRepositoryHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs index a542e75a7b..df0819b409 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs @@ -1,4 +1,6 @@ -using Bit.Core.Models.Data; +#nullable enable + +using Bit.Core.Models.Data; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 74833f38a0..bc2329930d 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -1,7 +1,6 @@ -using System.Text; -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; +#nullable enable + +using System.Text; using Microsoft.Extensions.Logging; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -10,94 +9,60 @@ namespace Bit.Core.Services; public class RabbitMqEventListenerService : EventLoggingListenerService { - private IChannel _channel; - private IConnection _connection; - private readonly string _exchangeName; - private readonly ConnectionFactory _factory; - private readonly ILogger _logger; + private readonly Lazy> _lazyChannel; private readonly string _queueName; + private readonly IRabbitMqService _rabbitMqService; public RabbitMqEventListenerService( IEventMessageHandler handler, - ILogger logger, - GlobalSettings globalSettings, - string queueName) : base(handler) + string queueName, + IRabbitMqService rabbitMqService, + ILogger logger) : base(handler, logger) { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _logger = logger; _queueName = queueName; + _rabbitMqService = rabbitMqService; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } public override async Task StartAsync(CancellationToken cancellationToken) { - _connection = await _factory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); - - await _channel.ExchangeDeclareAsync(exchange: _exchangeName, - type: ExchangeType.Fanout, - durable: true, - cancellationToken: cancellationToken); - await _channel.QueueDeclareAsync(queue: _queueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _queueName, - exchange: _exchangeName, - routingKey: string.Empty, - cancellationToken: cancellationToken); + await _rabbitMqService.CreateEventQueueAsync(_queueName, cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); - consumer.ReceivedAsync += async (_, eventArgs) => - { - try - { - using var jsonDocument = JsonDocument.Parse(Encoding.UTF8.GetString(eventArgs.Body.Span)); - var root = jsonDocument.RootElement; + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); + consumer.ReceivedAsync += async (_, eventArgs) => { await ProcessReceivedMessageAsync(eventArgs); }; - if (root.ValueKind == JsonValueKind.Array) - { - var eventMessages = root.Deserialize>(); - await _handler.HandleManyEventsAsync(eventMessages); - } - else if (root.ValueKind == JsonValueKind.Object) - { - var eventMessage = root.Deserialize(); - await _handler.HandleEventAsync(eventMessage); + await channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred while processing the message"); - } - }; - - await _channel.BasicConsumeAsync(_queueName, autoAck: true, consumer: consumer, cancellationToken: cancellationToken); + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs eventArgs) + { + await ProcessReceivedMessageAsync( + Encoding.UTF8.GetString(eventArgs.Body.Span), + eventArgs.BasicProperties.MessageId); } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs deleted file mode 100644 index 05fcf71a92..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text.Json; -using Bit.Core.Models.Data; -using Bit.Core.Settings; -using RabbitMQ.Client; - -namespace Bit.Core.Services; -public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable -{ - private readonly ConnectionFactory _factory; - private readonly Lazy> _lazyConnection; - private readonly string _exchangeName; - - public RabbitMqEventWriteService(GlobalSettings globalSettings) - { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; - - _lazyConnection = new Lazy>(CreateConnectionAsync); - } - - public async Task CreateAsync(IEvent e) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(e); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async Task CreateManyAsync(IEnumerable events) - { - var connection = await _lazyConnection.Value; - using var channel = await connection.CreateChannelAsync(); - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Fanout, durable: true); - - var body = JsonSerializer.SerializeToUtf8Bytes(events); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: string.Empty, body: body); - } - - public async ValueTask DisposeAsync() - { - if (_lazyConnection.IsValueCreated) - { - var connection = await _lazyConnection.Value; - await connection.DisposeAsync(); - } - } - - private async Task CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs index 1d6910db95..5b18d8817c 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -1,5 +1,8 @@ -using System.Text; -using Bit.Core.Settings; +#nullable enable + +using System.Text; +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; @@ -9,183 +12,137 @@ namespace Bit.Core.Services; public class RabbitMqIntegrationListenerService : BackgroundService { - private const string _deadLetterRoutingKey = "dead-letter"; - private IChannel _channel; - private IConnection _connection; - private readonly string _exchangeName; - private readonly string _queueName; - private readonly string _retryQueueName; - private readonly string _deadLetterQueueName; - private readonly string _routingKey; - private readonly string _retryRoutingKey; private readonly int _maxRetries; + private readonly string _queueName; + private readonly string _routingKey; + private readonly string _retryQueueName; private readonly IIntegrationHandler _handler; - private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyChannel; + private readonly IRabbitMqService _rabbitMqService; private readonly ILogger _logger; - private readonly int _retryTiming; public RabbitMqIntegrationListenerService(IIntegrationHandler handler, string routingKey, string queueName, string retryQueueName, - string deadLetterQueueName, - GlobalSettings globalSettings, + int maxRetries, + IRabbitMqService rabbitMqService, ILogger logger) { _handler = handler; _routingKey = routingKey; - _retryRoutingKey = $"{_routingKey}-retry"; - _queueName = queueName; _retryQueueName = retryQueueName; - _deadLetterQueueName = deadLetterQueueName; + _queueName = queueName; + _rabbitMqService = rabbitMqService; _logger = logger; - _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; - _maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries; - _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; - - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; + _maxRetries = maxRetries; + _lazyChannel = new Lazy>(() => _rabbitMqService.CreateChannelAsync()); } public override async Task StartAsync(CancellationToken cancellationToken) { - _connection = await _factory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); - - await _channel.ExchangeDeclareAsync(exchange: _exchangeName, - type: ExchangeType.Direct, - durable: true, - cancellationToken: cancellationToken); - - // Declare main queue - await _channel.QueueDeclareAsync(queue: _queueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _queueName, - exchange: _exchangeName, - routingKey: _routingKey, - cancellationToken: cancellationToken); - - // Declare retry queue (Configurable TTL, dead-letters back to main queue) - await _channel.QueueDeclareAsync(queue: _retryQueueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: new Dictionary - { - { "x-dead-letter-exchange", _exchangeName }, - { "x-dead-letter-routing-key", _routingKey }, - { "x-message-ttl", _retryTiming } - }, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _retryQueueName, - exchange: _exchangeName, - routingKey: _retryRoutingKey, - cancellationToken: cancellationToken); - - // Declare dead letter queue - await _channel.QueueDeclareAsync(queue: _deadLetterQueueName, - durable: true, - exclusive: false, - autoDelete: false, - arguments: null, - cancellationToken: cancellationToken); - await _channel.QueueBindAsync(queue: _deadLetterQueueName, - exchange: _exchangeName, - routingKey: _deadLetterRoutingKey, - cancellationToken: cancellationToken); + await _rabbitMqService.CreateIntegrationQueuesAsync( + _queueName, + _retryQueueName, + _routingKey, + cancellationToken: cancellationToken); await base.StartAsync(cancellationToken); } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - var consumer = new AsyncEventingBasicConsumer(_channel); + var channel = await _lazyChannel.Value; + var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += async (_, ea) => + { + await ProcessReceivedMessageAsync(ea, cancellationToken); + }; + + await channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + } + + internal async Task ProcessReceivedMessageAsync(BasicDeliverEventArgs ea, CancellationToken cancellationToken) + { + var channel = await _lazyChannel.Value; + try { var json = Encoding.UTF8.GetString(ea.Body.Span); - try + // Determine if the message came off of the retry queue too soon + // If so, place it back on the retry queue + var integrationMessage = JsonSerializer.Deserialize(json); + if (integrationMessage is not null && + integrationMessage.DelayUntilDate.HasValue && + integrationMessage.DelayUntilDate.Value > DateTime.UtcNow) { - var result = await _handler.HandleAsync(json); - var message = result.Message; + await _rabbitMqService.RepublishToRetryQueueAsync(channel, ea); + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } - if (result.Success) + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + // Successful integration send. Acknowledge message delivery and return + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } + + if (result.Retryable) + { + // Integration failed, but is retryable - apply delay and check max retries + message.ApplyRetry(result.DelayUntilDate); + + if (message.RetryCount < _maxRetries) { - // Successful integration send. Acknowledge message delivery and return - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); - return; - } - - if (result.Retryable) - { - // Integration failed, but is retryable - apply delay and check max retries - message.ApplyRetry(result.DelayUntilDate); - - if (message.RetryCount < _maxRetries) - { - // Publish message to the retry queue. It will be re-published for retry after a delay - await _channel.BasicPublishAsync( - exchange: _exchangeName, - routingKey: _retryRoutingKey, - body: Encoding.UTF8.GetBytes(message.ToJson()), - cancellationToken: cancellationToken); - } - else - { - // Exceeded the max number of retries; fail and send to dead letter queue - await PublishToDeadLetterAsync(message.ToJson()); - _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); - } + // Publish message to the retry queue. It will be re-published for retry after a delay + await _rabbitMqService.PublishToRetryAsync(channel, message, cancellationToken); } else { - // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries - await PublishToDeadLetterAsync(message.ToJson()); - _logger.LogWarning("Non-retryable failure. Sent to DLQ."); + // Exceeded the max number of retries; fail and send to dead letter queue + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); } - - // Message has been sent to retry or dead letter queues. - // Acknowledge receipt so Rabbit knows it's been processed - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); } - catch (Exception ex) + else { - // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error - _logger.LogError(ex, "Unhandled error processing integration message."); - await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries + await _rabbitMqService.PublishToDeadLetterAsync(channel, message, cancellationToken); + _logger.LogWarning("Non-retryable failure. Sent to DLQ."); } - }; - await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); - } - - private async Task PublishToDeadLetterAsync(string json) - { - await _channel.BasicPublishAsync( - exchange: _exchangeName, - routingKey: _deadLetterRoutingKey, - body: Encoding.UTF8.GetBytes(json)); + // Message has been sent to retry or dead letter queues. + // Acknowledge receipt so Rabbit knows it's been processed + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + catch (Exception ex) + { + // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error + _logger.LogError(ex, "Unhandled error processing integration message."); + await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } } public override async Task StopAsync(CancellationToken cancellationToken) { - await _channel.CloseAsync(cancellationToken); - await _connection.CloseAsync(cancellationToken); + if (_lazyChannel.IsValueCreated) + { + var channel = await _lazyChannel.Value; + await channel.CloseAsync(cancellationToken); + } await base.StopAsync(cancellationToken); } public override void Dispose() { - _channel.Dispose(); - _connection.Dispose(); + if (_lazyChannel.IsValueCreated && _lazyChannel.Value.IsCompletedSuccessfully) + { + _lazyChannel.Value.Result.Dispose(); + } base.Dispose(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs deleted file mode 100644 index 12801e3216..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Settings; -using RabbitMQ.Client; - -namespace Bit.Core.Services; - -public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable -{ - private readonly ConnectionFactory _factory; - private readonly Lazy> _lazyConnection; - private readonly string _exchangeName; - - public RabbitMqIntegrationPublisher(GlobalSettings globalSettings) - { - _factory = new ConnectionFactory - { - HostName = globalSettings.EventLogging.RabbitMq.HostName, - UserName = globalSettings.EventLogging.RabbitMq.Username, - Password = globalSettings.EventLogging.RabbitMq.Password - }; - _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; - - _lazyConnection = new Lazy>(CreateConnectionAsync); - } - - public async Task PublishAsync(IIntegrationMessage message) - { - var routingKey = message.IntegrationType.ToRoutingKey(); - var connection = await _lazyConnection.Value; - await using var channel = await connection.CreateChannelAsync(); - - await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true); - - var body = Encoding.UTF8.GetBytes(message.ToJson()); - - await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body); - } - - public async ValueTask DisposeAsync() - { - if (_lazyConnection.IsValueCreated) - { - var connection = await _lazyConnection.Value; - await connection.DisposeAsync(); - } - } - - private async Task CreateConnectionAsync() - { - return await _factory.CreateConnectionAsync(); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs new file mode 100644 index 0000000000..617d1b41fb --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqService.cs @@ -0,0 +1,244 @@ +#nullable enable + +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public class RabbitMqService : IRabbitMqService +{ + private const string _deadLetterRoutingKey = "dead-letter"; + + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _deadLetterQueueName; + private readonly string _eventExchangeName; + private readonly string _integrationExchangeName; + private readonly int _retryTiming; + private readonly bool _useDelayPlugin; + + public RabbitMqService(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _deadLetterQueueName = globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName; + _eventExchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; + _integrationExchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; + _useDelayPlugin = globalSettings.EventLogging.RabbitMq.UseDelayPlugin; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task CreateChannelAsync(CancellationToken cancellationToken = default) + { + var connection = await _lazyConnection.Value; + return await connection.CreateChannelAsync(cancellationToken: cancellationToken); + } + + public async Task CreateEventQueueAsync(string queueName, CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + await channel.QueueDeclareAsync(queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: queueName, + exchange: _eventExchangeName, + routingKey: string.Empty, + cancellationToken: cancellationToken); + } + + public async Task CreateIntegrationQueuesAsync( + string queueName, + string retryQueueName, + string routingKey, + CancellationToken cancellationToken = default) + { + using var channel = await CreateChannelAsync(cancellationToken); + var retryRoutingKey = $"{routingKey}-retry"; + + // Declare main integration queue + await channel.QueueDeclareAsync( + queue: queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await channel.QueueBindAsync( + queue: queueName, + exchange: _integrationExchangeName, + routingKey: routingKey, + cancellationToken: cancellationToken); + + if (!_useDelayPlugin) + { + // Declare retry queue (Configurable TTL, dead-letters back to main queue) + // Only needed if NOT using delay plugin + await channel.QueueDeclareAsync(queue: retryQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-dead-letter-exchange", _integrationExchangeName }, + { "x-dead-letter-routing-key", routingKey }, + { "x-message-ttl", _retryTiming } + }, + cancellationToken: cancellationToken); + await channel.QueueBindAsync(queue: retryQueueName, + exchange: _integrationExchangeName, + routingKey: retryRoutingKey, + cancellationToken: cancellationToken); + } + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + await using var channel = await CreateChannelAsync(); + + var body = Encoding.UTF8.GetBytes(message.ToJson()); + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: routingKey, + body: body); + } + + public async Task PublishEventAsync(string body) + { + await using var channel = await CreateChannelAsync(); + var properties = new BasicProperties + { + MessageId = Guid.NewGuid().ToString(), + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _eventExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: string.Empty, + body: Encoding.UTF8.GetBytes(body)); + } + + public async Task PublishToRetryAsync(IChannel channel, IIntegrationMessage message, CancellationToken cancellationToken) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + var retryRoutingKey = $"{routingKey}-retry"; + var properties = new BasicProperties + { + Persistent = true, + MessageId = message.MessageId, + Headers = _useDelayPlugin && message.DelayUntilDate.HasValue ? + new Dictionary + { + ["x-delay"] = Math.Max((int)(message.DelayUntilDate.Value - DateTime.UtcNow).TotalMilliseconds, 0) + } : + null + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: _useDelayPlugin ? routingKey : retryRoutingKey, + mandatory: true, + basicProperties: properties, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task PublishToDeadLetterAsync( + IChannel channel, + IIntegrationMessage message, + CancellationToken cancellationToken) + { + var properties = new BasicProperties + { + MessageId = message.MessageId, + Persistent = true + }; + + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + mandatory: true, + basicProperties: properties, + routingKey: _deadLetterRoutingKey, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + + public async Task RepublishToRetryQueueAsync(IChannel channel, BasicDeliverEventArgs eventArgs) + { + await channel.BasicPublishAsync( + exchange: _integrationExchangeName, + routingKey: eventArgs.RoutingKey, + mandatory: true, + basicProperties: new BasicProperties(eventArgs.BasicProperties), + body: eventArgs.Body); + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + var connection = await _factory.CreateConnectionAsync(); + using var channel = await connection.CreateChannelAsync(); + + // Declare Exchanges + await channel.ExchangeDeclareAsync(exchange: _eventExchangeName, type: ExchangeType.Fanout, durable: true); + if (_useDelayPlugin) + { + await channel.ExchangeDeclareAsync( + exchange: _integrationExchangeName, + type: "x-delayed-message", + durable: true, + arguments: new Dictionary + { + { "x-delayed-type", "direct" } + } + ); + } + else + { + await channel.ExchangeDeclareAsync(exchange: _integrationExchangeName, type: ExchangeType.Direct, durable: true); + } + + // Declare dead letter queue for Integration exchange + await channel.QueueDeclareAsync(queue: _deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null); + await channel.QueueBindAsync(queue: _deadLetterQueueName, + exchange: _integrationExchangeName, + routingKey: _deadLetterRoutingKey); + + return connection; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs index 134e93e838..fe0f6eabe1 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Models.Data.Integrations; +#nullable enable + +using Bit.Core.AdminConsole.Models.Data.Integrations; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/SlackService.cs b/src/Core/AdminConsole/Services/Implementations/SlackService.cs index effcfdf1ce..3f82217830 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackService.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackService.cs @@ -1,4 +1,6 @@ -using System.Net.Http.Headers; +#nullable enable + +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Web; using Bit.Core.Models.Slack; @@ -22,7 +24,7 @@ public class SlackService( public async Task GetChannelIdAsync(string token, string channelName) { - return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault(); + return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault() ?? string.Empty; } public async Task> GetChannelIdsAsync(string token, List channelNames) @@ -58,7 +60,7 @@ public class SlackService( } else { - logger.LogError("Error getting Channel Ids: {Error}", result.Error); + logger.LogError("Error getting Channel Ids: {Error}", result?.Error ?? "Unknown Error"); nextCursor = string.Empty; } @@ -89,7 +91,7 @@ public class SlackService( new KeyValuePair("redirect_uri", redirectUrl) })); - SlackOAuthResponse result; + SlackOAuthResponse? result; try { result = await tokenResponse.Content.ReadFromJsonAsync(); @@ -99,7 +101,7 @@ public class SlackService( result = null; } - if (result == null) + if (result is null) { logger.LogError("Error obtaining token via OAuth: Unknown error"); return string.Empty; @@ -130,6 +132,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error retrieving Slack user ID: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error retrieving Slack user ID: {Error}", result.Error); @@ -151,6 +158,11 @@ public class SlackService( var response = await _httpClient.SendAsync(request); var result = await response.Content.ReadFromJsonAsync(); + if (result is null) + { + logger.LogError("Error opening DM channel: Unknown error"); + return string.Empty; + } if (!result.Ok) { logger.LogError("Error opening DM channel: {Error}", result.Error); diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs index 5f9898afe8..df364b2a48 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs @@ -1,4 +1,6 @@ -using System.Globalization; +#nullable enable + +using System.Globalization; using System.Net; using System.Text; using Bit.Core.AdminConsole.Models.Data.Integrations; @@ -29,7 +31,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) case HttpStatusCode.ServiceUnavailable: case HttpStatusCode.GatewayTimeout: result.Retryable = true; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}"; if (response.Headers.TryGetValues("Retry-After", out var values)) { @@ -52,7 +54,7 @@ public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) break; default: result.Retryable = false; - result.FailureReason = response.ReasonPhrase; + result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}"; break; } diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index c647e825b6..0fd997fd10 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -78,13 +78,14 @@ public class OrganizationBillingService( var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization); var isManaged = organization.Status == OrganizationStatusType.Managed; - + var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { return OrganizationMetadata.Default with { IsEligibleForSelfHost = isEligibleForSelfHost, - IsManaged = isManaged + IsManaged = isManaged, + OrganizationOccupiedSeats = orgOccupiedSeats }; } @@ -108,8 +109,6 @@ public class OrganizationBillingService( ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) : null; - var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); - return new OrganizationMetadata( isEligibleForSelfHost, isManaged, @@ -420,7 +419,7 @@ public class OrganizationBillingService( var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); - if (setNonUSBusinessUseToReverseCharge) + if (setNonUSBusinessUseToReverseCharge && customer.HasBillingLocation()) { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8527aa0694..e6a822452a 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -145,7 +145,6 @@ public static class FeatureFlagKeys public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; - public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; diff --git a/src/Core/Exceptions/BadRequestException.cs b/src/Core/Exceptions/BadRequestException.cs index 042f853a57..b27bc7510f 100644 --- a/src/Core/Exceptions/BadRequestException.cs +++ b/src/Core/Exceptions/BadRequestException.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; namespace Bit.Core.Exceptions; +#nullable enable + public class BadRequestException : Exception { public BadRequestException() : base() @@ -41,5 +43,5 @@ public class BadRequestException : Exception } } - public ModelStateDictionary ModelState { get; set; } + public ModelStateDictionary? ModelState { get; set; } } diff --git a/src/Core/Exceptions/ConflictException.cs b/src/Core/Exceptions/ConflictException.cs index 27b90a657f..92fcc52d7f 100644 --- a/src/Core/Exceptions/ConflictException.cs +++ b/src/Core/Exceptions/ConflictException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class ConflictException : Exception { public ConflictException() : base("Conflict.") { } diff --git a/src/Core/Exceptions/DnsQueryException.cs b/src/Core/Exceptions/DnsQueryException.cs index 57b2c56daa..e3f605dec4 100644 --- a/src/Core/Exceptions/DnsQueryException.cs +++ b/src/Core/Exceptions/DnsQueryException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DnsQueryException : Exception { public DnsQueryException(string message) diff --git a/src/Core/Exceptions/DomainClaimedException.cs b/src/Core/Exceptions/DomainClaimedException.cs index 09ccb3d0d8..9ac6972fa1 100644 --- a/src/Core/Exceptions/DomainClaimedException.cs +++ b/src/Core/Exceptions/DomainClaimedException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DomainClaimedException : Exception { public DomainClaimedException() diff --git a/src/Core/Exceptions/DomainVerifiedException.cs b/src/Core/Exceptions/DomainVerifiedException.cs index d3a3fd4de4..1fb704bd55 100644 --- a/src/Core/Exceptions/DomainVerifiedException.cs +++ b/src/Core/Exceptions/DomainVerifiedException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DomainVerifiedException : Exception { public DomainVerifiedException() diff --git a/src/Core/Exceptions/DuplicateDomainException.cs b/src/Core/Exceptions/DuplicateDomainException.cs index 8d347dda55..4f61f333f5 100644 --- a/src/Core/Exceptions/DuplicateDomainException.cs +++ b/src/Core/Exceptions/DuplicateDomainException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class DuplicateDomainException : Exception { public DuplicateDomainException() diff --git a/src/Core/Exceptions/FeatureUnavailableException.cs b/src/Core/Exceptions/FeatureUnavailableException.cs index 7bea350956..80fd7d0635 100644 --- a/src/Core/Exceptions/FeatureUnavailableException.cs +++ b/src/Core/Exceptions/FeatureUnavailableException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + /// /// Exception to throw when a requested feature is not yet enabled/available for the requesting context. /// diff --git a/src/Core/Exceptions/GatewayException.cs b/src/Core/Exceptions/GatewayException.cs index 73e8cd7613..4b24c8d107 100644 --- a/src/Core/Exceptions/GatewayException.cs +++ b/src/Core/Exceptions/GatewayException.cs @@ -1,8 +1,10 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class GatewayException : Exception { - public GatewayException(string message, Exception innerException = null) + public GatewayException(string message, Exception? innerException = null) : base(message, innerException) { } } diff --git a/src/Core/Exceptions/InvalidEmailException.cs b/src/Core/Exceptions/InvalidEmailException.cs index 1f17acf62e..c38ec0ac38 100644 --- a/src/Core/Exceptions/InvalidEmailException.cs +++ b/src/Core/Exceptions/InvalidEmailException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class InvalidEmailException : Exception { public InvalidEmailException() diff --git a/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs b/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs index cfc7c56c1c..6ec15da308 100644 --- a/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs +++ b/src/Core/Exceptions/InvalidGatewayCustomerIdException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class InvalidGatewayCustomerIdException : Exception { public InvalidGatewayCustomerIdException() diff --git a/src/Core/Exceptions/NotFoundException.cs b/src/Core/Exceptions/NotFoundException.cs index 70769d41ed..6a61e35868 100644 --- a/src/Core/Exceptions/NotFoundException.cs +++ b/src/Core/Exceptions/NotFoundException.cs @@ -1,5 +1,7 @@ namespace Bit.Core.Exceptions; +#nullable enable + public class NotFoundException : Exception { public NotFoundException() : base() diff --git a/src/Core/HostedServices/ApplicationCacheHostedService.cs b/src/Core/HostedServices/ApplicationCacheHostedService.cs index 9021782d20..a699a26fcc 100644 --- a/src/Core/HostedServices/ApplicationCacheHostedService.cs +++ b/src/Core/HostedServices/ApplicationCacheHostedService.cs @@ -10,9 +10,11 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.HostedServices; +#nullable enable + public class ApplicationCacheHostedService : IHostedService, IDisposable { - private readonly InMemoryServiceBusApplicationCacheService _applicationCacheService; + private readonly InMemoryServiceBusApplicationCacheService? _applicationCacheService; private readonly IOrganizationRepository _organizationRepository; protected readonly ILogger _logger; private readonly ServiceBusClient _serviceBusClient; @@ -20,8 +22,8 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable private readonly ServiceBusAdministrationClient _serviceBusAdministrationClient; private readonly string _subName; private readonly string _topicName; - private CancellationTokenSource _cts; - private Task _executingTask; + private CancellationTokenSource? _cts; + private Task? _executingTask; public ApplicationCacheHostedService( @@ -67,13 +69,17 @@ public class ApplicationCacheHostedService : IHostedService, IDisposable { await _subscriptionReceiver.CloseAsync(cancellationToken); await _serviceBusClient.DisposeAsync(); - _cts.Cancel(); + _cts?.Cancel(); try { await _serviceBusAdministrationClient.DeleteSubscriptionAsync(_topicName, _subName, cancellationToken); } catch { } - await _executingTask; + + if (_executingTask != null) + { + await _executingTask; + } } public virtual void Dispose() diff --git a/src/Core/HostedServices/IpRateLimitSeedStartupService.cs b/src/Core/HostedServices/IpRateLimitSeedStartupService.cs index a6869d929c..827dd94806 100644 --- a/src/Core/HostedServices/IpRateLimitSeedStartupService.cs +++ b/src/Core/HostedServices/IpRateLimitSeedStartupService.cs @@ -3,6 +3,8 @@ using Microsoft.Extensions.Hosting; namespace Bit.Core.HostedServices; +#nullable enable + /// /// A startup service that will seed the IP rate limiting stores with any values in the /// GlobalSettings configuration. diff --git a/src/Core/Jobs/BaseJob.cs b/src/Core/Jobs/BaseJob.cs index 56c39014a7..a56045f659 100644 --- a/src/Core/Jobs/BaseJob.cs +++ b/src/Core/Jobs/BaseJob.cs @@ -3,6 +3,8 @@ using Quartz; namespace Bit.Core.Jobs; +#nullable enable + public abstract class BaseJob : IJob { protected readonly ILogger _logger; diff --git a/src/Core/Jobs/BaseJobsHostedService.cs b/src/Core/Jobs/BaseJobsHostedService.cs index 897a382a2b..2ade53c6bb 100644 --- a/src/Core/Jobs/BaseJobsHostedService.cs +++ b/src/Core/Jobs/BaseJobsHostedService.cs @@ -8,6 +8,8 @@ using Quartz.Impl.Matchers; namespace Bit.Core.Jobs; +#nullable enable + public abstract class BaseJobsHostedService : IHostedService, IDisposable { private const int MaximumJobRetries = 10; @@ -16,7 +18,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable private readonly ILogger _listenerLogger; protected readonly ILogger _logger; - private IScheduler _scheduler; + private IScheduler? _scheduler; protected GlobalSettings _globalSettings; public BaseJobsHostedService( @@ -31,7 +33,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable _globalSettings = globalSettings; } - public IEnumerable> Jobs { get; protected set; } + public IEnumerable>? Jobs { get; protected set; } public virtual async Task StartAsync(CancellationToken cancellationToken) { @@ -61,10 +63,19 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable _scheduler.ListenerManager.AddJobListener(new JobListener(_listenerLogger), GroupMatcher.AnyGroup()); await _scheduler.Start(cancellationToken); + + var jobKeys = new List(); + var triggerKeys = new List(); + if (Jobs != null) { foreach (var (job, trigger) in Jobs) { + jobKeys.Add(JobBuilder.Create(job) + .WithIdentity(job.FullName!) + .Build().Key); + triggerKeys.Add(trigger.Key); + for (var retry = 0; retry < MaximumJobRetries; retry++) { // There's a race condition when starting multiple containers simultaneously, retry until it succeeds.. @@ -77,7 +88,7 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable } var jobDetail = JobBuilder.Create(job) - .WithIdentity(job.FullName) + .WithIdentity(job.FullName!) .Build(); var dupeJ = await _scheduler.GetJobDetail(jobDetail.Key); @@ -106,13 +117,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable // Delete old Jobs and Triggers var existingJobKeys = await _scheduler.GetJobKeys(GroupMatcher.AnyGroup()); - var jobKeys = Jobs.Select(j => - { - var job = j.Item1; - return JobBuilder.Create(job) - .WithIdentity(job.FullName) - .Build().Key; - }); foreach (var key in existingJobKeys) { @@ -126,7 +130,6 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable } var existingTriggerKeys = await _scheduler.GetTriggerKeys(GroupMatcher.AnyGroup()); - var triggerKeys = Jobs.Select(j => j.Item2.Key); foreach (var key in existingTriggerKeys) { @@ -142,7 +145,10 @@ public abstract class BaseJobsHostedService : IHostedService, IDisposable public virtual async Task StopAsync(CancellationToken cancellationToken) { - await _scheduler?.Shutdown(cancellationToken); + if (_scheduler is not null) + { + await _scheduler.Shutdown(cancellationToken); + } } public virtual void Dispose() diff --git a/src/Core/Jobs/JobFactory.cs b/src/Core/Jobs/JobFactory.cs index 6529443d97..8289a90322 100644 --- a/src/Core/Jobs/JobFactory.cs +++ b/src/Core/Jobs/JobFactory.cs @@ -4,6 +4,8 @@ using Quartz.Spi; namespace Bit.Core.Jobs; +#nullable enable + public class JobFactory : IJobFactory { private readonly IServiceProvider _container; @@ -16,7 +18,7 @@ public class JobFactory : IJobFactory public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { var scope = _container.CreateScope(); - return scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob; + return (scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob)!; } public void ReturnJob(IJob job) diff --git a/src/Core/Jobs/JobListener.cs b/src/Core/Jobs/JobListener.cs index e5e05e4b6b..0dc865655d 100644 --- a/src/Core/Jobs/JobListener.cs +++ b/src/Core/Jobs/JobListener.cs @@ -3,6 +3,8 @@ using Quartz; namespace Bit.Core.Jobs; +#nullable enable + public class JobListener : IJobListener { private readonly ILogger _logger; @@ -28,7 +30,7 @@ public class JobListener : IJobListener return Task.FromResult(0); } - public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException, + public Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default(CancellationToken)) { _logger.LogInformation(Constants.BypassFiltersEventId, null, "Finished job {0} at {1}.", diff --git a/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs index 2e68325e1d..f81811eb13 100644 --- a/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs +++ b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; -using Bit.Core.Enums; +using Bit.Core.KeyManagement.Enums; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; diff --git a/src/Core/Enums/SignatureAlgorithm.cs b/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs similarity index 80% rename from src/Core/Enums/SignatureAlgorithm.cs rename to src/Core/KeyManagement/Enums/SignatureAlgorithm.cs index 51360e5a0c..9216c3f489 100644 --- a/src/Core/Enums/SignatureAlgorithm.cs +++ b/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Enums; +namespace Bit.Core.KeyManagement.Enums; // // Represents the algorithm / digital signature scheme used for a signature key pair. diff --git a/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs b/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs index af66468b1f..7e64dc76e3 100644 --- a/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs +++ b/src/Core/KeyManagement/Models/Data/SignatureKeyPairData.cs @@ -1,7 +1,7 @@ #nullable enable using System.Text.Json.Serialization; -using Bit.Core.Enums; +using Bit.Core.KeyManagement.Enums; namespace Bit.Core.KeyManagement.Models.Data; diff --git a/src/Core/NotificationHub/INotificationHubClientProxy.cs b/src/Core/NotificationHub/INotificationHubClientProxy.cs index 82b4d39591..78eb0206d6 100644 --- a/src/Core/NotificationHub/INotificationHubClientProxy.cs +++ b/src/Core/NotificationHub/INotificationHubClientProxy.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public interface INotificationHubProxy { Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary properties, string tagExpression); diff --git a/src/Core/NotificationHub/INotificationHubPool.cs b/src/Core/NotificationHub/INotificationHubPool.cs index 3981598118..25a31d62f4 100644 --- a/src/Core/NotificationHub/INotificationHubPool.cs +++ b/src/Core/NotificationHub/INotificationHubPool.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public interface INotificationHubPool { NotificationHubConnection ConnectionFor(Guid comb); diff --git a/src/Core/NotificationHub/NotificationHubClientProxy.cs b/src/Core/NotificationHub/NotificationHubClientProxy.cs index 815ac88363..b47069fe21 100644 --- a/src/Core/NotificationHub/NotificationHubClientProxy.cs +++ b/src/Core/NotificationHub/NotificationHubClientProxy.cs @@ -2,6 +2,8 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubClientProxy : INotificationHubProxy { private readonly IEnumerable _clients; diff --git a/src/Core/NotificationHub/NotificationHubConnection.cs b/src/Core/NotificationHub/NotificationHubConnection.cs index a68134450e..a61f2ded8f 100644 --- a/src/Core/NotificationHub/NotificationHubConnection.cs +++ b/src/Core/NotificationHub/NotificationHubConnection.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; using System.Text; using System.Web; using Bit.Core.Settings; @@ -7,21 +8,23 @@ using Microsoft.Azure.NotificationHubs; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubConnection { - public string HubName { get; init; } - public string ConnectionString { get; init; } + public string? HubName { get; init; } + public string? ConnectionString { get; init; } private Lazy _parsedConnectionString; public Uri Endpoint => _parsedConnectionString.Value.Endpoint; private string SasKey => _parsedConnectionString.Value.SharedAccessKey; private string SasKeyName => _parsedConnectionString.Value.SharedAccessKeyName; public bool EnableSendTracing { get; init; } - private NotificationHubClient _hubClient; + private NotificationHubClient? _hubClient; /// /// Gets the NotificationHubClient for this connection. - /// + /// /// If the client is null, it will be initialized. - /// + /// /// Exception if the connection is invalid. /// public NotificationHubClient HubClient @@ -45,13 +48,13 @@ public class NotificationHubConnection } /// /// Gets the start date for registration. - /// + /// /// If null, registration is always disabled. /// public DateTime? RegistrationStartDate { get; init; } /// /// Gets the end date for registration. - /// + /// /// If null, registration has no end date. /// public DateTime? RegistrationEndDate { get; init; } @@ -155,9 +158,10 @@ public class NotificationHubConnection }; } + [MemberNotNull(nameof(_hubClient))] private NotificationHubConnection Init() { - HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); + _hubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing); return this; } diff --git a/src/Core/NotificationHub/NotificationHubPool.cs b/src/Core/NotificationHub/NotificationHubPool.cs index 6b48e82f88..38192c11fc 100644 --- a/src/Core/NotificationHub/NotificationHubPool.cs +++ b/src/Core/NotificationHub/NotificationHubPool.cs @@ -5,6 +5,8 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubPool : INotificationHubPool { private List _connections { get; } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index bb3de80977..368c0f731b 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -19,6 +19,8 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; +#nullable enable + /// /// Sends mobile push notifications to the Azure Notification Hub. /// Used by Cloud-Hosted environments. diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index f44fcf91a0..dc494eecd6 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.NotificationHub; +#nullable enable + public class NotificationHubPushRegistrationService : IPushRegistrationService { private static readonly JsonSerializerOptions webPushSerializationOptions = new() @@ -37,7 +39,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } public async Task CreateOrUpdateRegistrationAsync(PushRegistrationData data, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) + string? identifier, DeviceType type, IEnumerable organizationIds, Guid installationId) { var orgIds = organizationIds.ToList(); var clientType = DeviceTypes.ToClientType(type); @@ -79,7 +81,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private async Task CreateOrUpdateMobileRegistrationAsync(Installation installation, string userId, - string identifier, ClientType clientType, List organizationIds, DeviceType type, Guid installationId) + string? identifier, ClientType clientType, List organizationIds, DeviceType type, Guid installationId) { if (string.IsNullOrWhiteSpace(installation.PushChannel)) { @@ -137,7 +139,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private async Task CreateOrUpdateWebRegistrationAsync(string endpoint, string p256dh, string auth, Installation installation, string userId, - string identifier, ClientType clientType, List organizationIds, Guid installationId) + string? identifier, ClientType clientType, List organizationIds, Guid installationId) { // The Azure SDK is currently lacking support for web push registrations. // We need to use the REST API directly. @@ -187,7 +189,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private static KeyValuePair BuildInstallationTemplate(string templateId, [StringSyntax(StringSyntaxAttribute.Json)] string templateBody, - string userId, string identifier, ClientType clientType, List organizationIds, Guid installationId) + string userId, string? identifier, ClientType clientType, List organizationIds, Guid installationId) { var fullTemplateId = $"template:{templateId}"; diff --git a/src/Core/NotificationHub/PushRegistrationData.cs b/src/Core/NotificationHub/PushRegistrationData.cs index 20e1cf0936..c11ee7be23 100644 --- a/src/Core/NotificationHub/PushRegistrationData.cs +++ b/src/Core/NotificationHub/PushRegistrationData.cs @@ -1,5 +1,7 @@ namespace Bit.Core.NotificationHub; +#nullable enable + public record struct WebPushRegistrationData { public string Endpoint { get; init; } @@ -9,9 +11,9 @@ public record struct WebPushRegistrationData public record class PushRegistrationData { - public string Token { get; set; } + public string? Token { get; set; } public WebPushRegistrationData? WebPush { get; set; } - public PushRegistrationData(string token) + public PushRegistrationData(string? token) { Token = token; } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index b821f214db..f08d66c28f 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -327,6 +327,7 @@ public class GlobalSettings : IGlobalSettings public int MaxRetries { get; set; } = 3; public int RetryTiming { get; set; } = 30000; // 30s + public bool UseDelayPlugin { get; set; } = false; public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue"; public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue"; diff --git a/src/Core/Vault/Services/ICipherService.cs b/src/Core/Vault/Services/ICipherService.cs index 3721f592ba..d3f8d20c90 100644 --- a/src/Core/Vault/Services/ICipherService.cs +++ b/src/Core/Vault/Services/ICipherService.cs @@ -24,7 +24,7 @@ public interface ICipherService Task DeleteFolderAsync(Folder folder); Task ShareAsync(Cipher originalCipher, Cipher cipher, Guid organizationId, IEnumerable collectionIds, Guid userId, DateTime? lastKnownRevisionDate); - Task> ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, + Task> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> ciphers, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId); Task SaveCollectionsAsync(Cipher cipher, IEnumerable collectionIds, Guid savingUserId, bool orgAdmin); Task SoftDeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false); diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index cf4e1fda86..413aee3e0d 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -625,7 +625,7 @@ public class CipherService : ICipherService await _pushService.PushSyncCipherUpdateAsync(cipher, collectionIds); } - public async Task> ShareManyAsync(IEnumerable<(Cipher cipher, DateTime? lastKnownRevisionDate)> cipherInfos, + public async Task> ShareManyAsync(IEnumerable<(CipherDetails cipher, DateTime? lastKnownRevisionDate)> cipherInfos, Guid organizationId, IEnumerable collectionIds, Guid sharingUserId) { var cipherIds = new List(); diff --git a/src/Icons/Controllers/IconsController.cs b/src/Icons/Controllers/IconsController.cs index 871219b366..b0d9306f94 100644 --- a/src/Icons/Controllers/IconsController.cs +++ b/src/Icons/Controllers/IconsController.cs @@ -8,16 +8,6 @@ namespace Bit.Icons.Controllers; [Route("")] public class IconsController : Controller { - // Basic bwi-globe icon - private static readonly byte[] _notFoundImage = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUg" + - "AAABMAAAATCAQAAADYWf5HAAABu0lEQVR42nXSvWuTURTH8R+t0heI9Y04aJycdBLNJNrBFBU7OFgUER3q21I0bXK+JwZ" + - "pXISm/QdcRB3EgqBBsNihsUbbgODQQSKCuKSDOApJuuhj8tCYQj/jvYfD795z1MZ+nBKrNKhSwrMxbZTrtRnqlEjZkB/x" + - "C/xmhZrlc71qS0Up8yVzTCGucFNKD1JhORVd70SZNU4okNx5d4+U2UXRIpJFWLClsR79YzN88wQvLWNzzPKEeS/wkQGpW" + - "VhhqhW8TtDJD3Mm1x/23zLSrZCdpBY8BueTNjHSbc+8wC9HlHgU5Aj5AW5zPdcVdpq0UcknWBSr/pjixO4gfp899Kd23p" + - "M2qQCH7LkCnqAqGh73OK/8NPOcaibr90LrW/yWAnaUhqjaOSl9nFR2r5rsqo22ypn1B5IN8VOUMHVgOnNQIX+d62plcz6" + - "rg1/jskK8CMb4we4pG6OWHtR/LBJkC2E4a7ZPkuX5ntumAOM2xxveclEhLvGH6XCmLPs735Eetrw63NnOgr9P9q1viC3x" + - "lRUGOjImqFDuOBvrYYoaZU9z1uPpYae5NfdvbNVG2ZjDIlXq/oMi46lo++4vjjPBl2Dlg00AAAAASUVORK5CYII="); - private readonly IMemoryCache _memoryCache; private readonly IDomainMappingService _domainMappingService; private readonly IIconFetchingService _iconFetchingService; @@ -99,7 +89,7 @@ public class IconsController : Controller if (icon == null) { - return new FileContentResult(_notFoundImage, "image/png"); + return new NotFoundResult(); } return new FileContentResult(icon.Image, icon.Format); diff --git a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs index a6a9b461cd..5e7353f526 100644 --- a/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs +++ b/src/Infrastructure.Dapper/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -5,6 +5,7 @@ using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Repositories; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; using Dapper; using Microsoft.Data.SqlClient; @@ -45,7 +46,7 @@ public class UserSignatureKeyPairRepository : Repository("broadcast"); + services.AddSingleton(); + services.AddKeyedSingleton("broadcast"); } else { @@ -563,7 +564,8 @@ public static class ServiceCollectionExtensions if (IsRabbitMqEnabled(globalSettings)) { - services.AddKeyedSingleton("broadcast"); + services.AddSingleton(); + services.AddKeyedSingleton("broadcast"); } else { @@ -585,13 +587,15 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => new AzureServiceBusEventListenerService( handler: provider.GetRequiredService(), - logger: provider.GetRequiredService>(), + serviceBusService: provider.GetRequiredService(), + subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName, globalSettings: globalSettings, - subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + logger: provider.GetRequiredService>() + ) + ); return services; } @@ -607,12 +611,10 @@ public static class ServiceCollectionExtensions { var routingKey = integrationType.ToRoutingKey(); - services.AddSingleton(); - services.AddKeyedSingleton(routingKey, (provider, _) => new EventIntegrationHandler( integrationType, - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService())); @@ -620,18 +622,22 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new AzureServiceBusEventListenerService( handler: provider.GetRequiredKeyedService(routingKey), - logger: provider.GetRequiredService>(), + serviceBusService: provider.GetRequiredService(), + subscriptionName: eventSubscriptionName, globalSettings: globalSettings, - subscriptionName: eventSubscriptionName)); + logger: provider.GetRequiredService>() + ) + ); services.AddSingleton, THandler>(); - services.AddSingleton(provider => new AzureServiceBusIntegrationListenerService( handler: provider.GetRequiredService>(), + topicName: globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName, subscriptionName: integrationSubscriptionName, - logger: provider.GetRequiredService>(), - globalSettings: globalSettings)); + maxRetries: globalSettings.EventLogging.AzureServiceBus.MaxRetries, + serviceBusService: provider.GetRequiredService(), + logger: provider.GetRequiredService>())); return services; } @@ -642,6 +648,8 @@ public static class ServiceCollectionExtensions !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) return services; + services.AddSingleton(); + services.AddSingleton(); services.AddAzureServiceBusEventRepositoryListener(globalSettings); services.AddSlackService(globalSettings); @@ -668,9 +676,9 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new RabbitMqEventListenerService( provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName, + provider.GetRequiredService(), + provider.GetRequiredService>())); return services; } @@ -679,19 +687,17 @@ public static class ServiceCollectionExtensions string eventQueueName, string integrationQueueName, string integrationRetryQueueName, - string integrationDeadLetterQueueName, - IntegrationType integrationType, - GlobalSettings globalSettings) + int maxRetries, + IntegrationType integrationType) where TConfig : class where THandler : class, IIntegrationHandler { var routingKey = integrationType.ToRoutingKey(); - services.AddSingleton(); services.AddKeyedSingleton(routingKey, (provider, _) => new EventIntegrationHandler( integrationType, - provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService())); @@ -699,9 +705,9 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => new RabbitMqEventListenerService( provider.GetRequiredKeyedService(routingKey), - provider.GetRequiredService>(), - globalSettings, - eventQueueName)); + eventQueueName, + provider.GetRequiredService(), + provider.GetRequiredService>())); services.AddSingleton, THandler>(); services.AddSingleton(provider => @@ -710,8 +716,8 @@ public static class ServiceCollectionExtensions routingKey: routingKey, queueName: integrationQueueName, retryQueueName: integrationRetryQueueName, - deadLetterQueueName: integrationDeadLetterQueueName, - globalSettings: globalSettings, + maxRetries: maxRetries, + rabbitMqService: provider.GetRequiredService(), logger: provider.GetRequiredService>())); return services; @@ -724,6 +730,8 @@ public static class ServiceCollectionExtensions return services; } + services.AddSingleton(); + services.AddSingleton(); services.AddRabbitMqEventRepositoryListener(globalSettings); services.AddSlackService(globalSettings); @@ -731,18 +739,16 @@ public static class ServiceCollectionExtensions globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Slack, - globalSettings); + globalSettings.EventLogging.RabbitMq.MaxRetries, + IntegrationType.Slack); services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); services.AddRabbitMqIntegration( globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Webhook, - globalSettings); + globalSettings.EventLogging.RabbitMq.MaxRetries, + IntegrationType.Webhook); return services; } diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Create.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Create.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Create.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_DeleteById.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_DeleteById.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_DeleteIfExpired.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteIfExpired.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_DeleteIfExpired.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_DeleteIfExpired.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadAdminApprovalsByIds.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadAdminApprovalsByIds.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadAdminApprovalsByIds.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadAdminApprovalsByIds.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadById.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadById.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadByUserId.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadByUserId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadByUserId.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadByUserId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadPendingByOrganizationId.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByOrganizationId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_ReadPendingByOrganizationId.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_ReadPendingByOrganizationId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Update.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_Update.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_Update.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql b/src/Sql/dbo/Auth/Stored Procedures/AuthRequest_UpdateMany.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/AuthRequest_UpdateMany.sql rename to src/Sql/dbo/Auth/Stored Procedures/AuthRequest_UpdateMany.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql b/src/Sql/dbo/Auth/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql rename to src/Sql/dbo/Auth/Stored Procedures/Device_ReadActiveWithPendingAuthRequestsByUserId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByGranteeId.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGranteeId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByGranteeId.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGranteeId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByGrantorId.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGrantorId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByGrantorId.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByGrantorId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByIdGrantorId.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByIdGrantorId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadByIdGrantorId.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadByIdGrantorId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadExpiredRecoveries.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadExpiredRecoveries.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccessDetails_ReadExpiredRecoveries.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccessDetails_ReadExpiredRecoveries.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_Create.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Create.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_Create.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Create.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_DeleteById.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_DeleteById.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_DeleteById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadById.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadById.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadCountByGrantorIdEmail.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadCountByGrantorIdEmail.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadCountByGrantorIdEmail.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadCountByGrantorIdEmail.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadToNotify.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadToNotify.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_ReadToNotify.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_ReadToNotify.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_Update.sql b/src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Update.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/EmergencyAccess_Update.sql rename to src/Sql/dbo/Auth/Stored Procedures/EmergencyAccess_Update.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_Delete.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_Delete.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_Delete.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_Delete.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_DeleteByKey.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteByKey.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_DeleteByKey.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteByKey.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_DeleteExpired.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteExpired.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_DeleteExpired.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_DeleteExpired.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_Read.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_Read.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_Read.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_Read.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_ReadByKey.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_ReadByKey.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_ReadByKey.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_ReadByKey.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/Grant_Save.sql b/src/Sql/dbo/Auth/Stored Procedures/Grant_Save.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/Grant_Save.sql rename to src/Sql/dbo/Auth/Stored Procedures/Grant_Save.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_Create.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Create.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_Create.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Create.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_DeleteById.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_DeleteById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_DeleteById.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_DeleteById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadById.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadById.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadByIdentifier.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByIdentifier.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadByIdentifier.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByIdentifier.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadByOrganizationId.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByOrganizationId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadByOrganizationId.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadByOrganizationId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadManyByNotBeforeRevisionDate.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadManyByNotBeforeRevisionDate.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_ReadManyByNotBeforeRevisionDate.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_ReadManyByNotBeforeRevisionDate.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoConfig_Update.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Update.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoConfig_Update.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoConfig_Update.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_Create.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_Create.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Delete.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_Delete.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_Delete.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_Delete.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_DeleteById.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_DeleteById.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_DeleteMany.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteMany.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_DeleteMany.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_DeleteMany.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_ReadById.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadById.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_ReadById.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadById.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_ReadByUserIdOrganizationId.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadByUserIdOrganizationId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_ReadByUserIdOrganizationId.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_ReadByUserIdOrganizationId.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql b/src/Sql/dbo/Auth/Stored Procedures/SsoUser_Update.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql rename to src/Sql/dbo/Auth/Stored Procedures/SsoUser_Update.sql diff --git a/src/Sql/Auth/dbo/Stored Procedures/User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql b/src/Sql/dbo/Auth/Stored Procedures/User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql similarity index 100% rename from src/Sql/Auth/dbo/Stored Procedures/User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql rename to src/Sql/dbo/Auth/Stored Procedures/User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql diff --git a/src/Sql/Auth/dbo/Tables/AuthRequest.sql b/src/Sql/dbo/Auth/Tables/AuthRequest.sql similarity index 100% rename from src/Sql/Auth/dbo/Tables/AuthRequest.sql rename to src/Sql/dbo/Auth/Tables/AuthRequest.sql diff --git a/src/Sql/Auth/dbo/Tables/EmergencyAccess.sql b/src/Sql/dbo/Auth/Tables/EmergencyAccess.sql similarity index 100% rename from src/Sql/Auth/dbo/Tables/EmergencyAccess.sql rename to src/Sql/dbo/Auth/Tables/EmergencyAccess.sql diff --git a/src/Sql/Auth/dbo/Tables/Grant.sql b/src/Sql/dbo/Auth/Tables/Grant.sql similarity index 100% rename from src/Sql/Auth/dbo/Tables/Grant.sql rename to src/Sql/dbo/Auth/Tables/Grant.sql diff --git a/src/Sql/Auth/dbo/Tables/SsoConfig.sql b/src/Sql/dbo/Auth/Tables/SsoConfig.sql similarity index 100% rename from src/Sql/Auth/dbo/Tables/SsoConfig.sql rename to src/Sql/dbo/Auth/Tables/SsoConfig.sql diff --git a/src/Sql/Auth/dbo/Tables/SsoUser.sql b/src/Sql/dbo/Auth/Tables/SsoUser.sql similarity index 100% rename from src/Sql/Auth/dbo/Tables/SsoUser.sql rename to src/Sql/dbo/Auth/Tables/SsoUser.sql diff --git a/src/Sql/Auth/dbo/Views/AuthRequestView.sql b/src/Sql/dbo/Auth/Views/AuthRequestView.sql similarity index 100% rename from src/Sql/Auth/dbo/Views/AuthRequestView.sql rename to src/Sql/dbo/Auth/Views/AuthRequestView.sql diff --git a/src/Sql/Auth/dbo/Views/EmergencyAccessDetailsView.sql b/src/Sql/dbo/Auth/Views/EmergencyAccessDetailsView.sql similarity index 100% rename from src/Sql/Auth/dbo/Views/EmergencyAccessDetailsView.sql rename to src/Sql/dbo/Auth/Views/EmergencyAccessDetailsView.sql diff --git a/src/Sql/Auth/dbo/Views/GrantView.sql b/src/Sql/dbo/Auth/Views/GrantView.sql similarity index 100% rename from src/Sql/Auth/dbo/Views/GrantView.sql rename to src/Sql/dbo/Auth/Views/GrantView.sql diff --git a/src/Sql/Auth/dbo/Views/SsoConfigView.sql b/src/Sql/dbo/Auth/Views/SsoConfigView.sql similarity index 100% rename from src/Sql/Auth/dbo/Views/SsoConfigView.sql rename to src/Sql/dbo/Auth/Views/SsoConfigView.sql diff --git a/src/Sql/Auth/dbo/Views/SsoUserView.sql b/src/Sql/dbo/Auth/Views/SsoUserView.sql similarity index 100% rename from src/Sql/Auth/dbo/Views/SsoUserView.sql rename to src/Sql/dbo/Auth/Views/SsoUserView.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Create.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_DeleteById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByOrganizationId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_ReadByProviderId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql b/src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql rename to src/Sql/dbo/Billing/Stored Procedures/ClientOrganizationMigrationRecord_Update.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Create.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Create.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Create.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_DeleteById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_DeleteById.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_DeleteById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadById.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByInstallationId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_ReadByOrganizationId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql b/src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Update.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/OrganizationInstallation_Update.sql rename to src/Sql/dbo/Billing/Stored Procedures/OrganizationInstallation_Update.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Create.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Create.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Create.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Create.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_DeleteById.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_DeleteById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_DeleteById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_DeleteById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadById.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByInvoiceId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_ReadByProviderId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Update.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Update.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderInvoiceItem_Update.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderInvoiceItem_Update.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Create.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Create.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Create.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Create.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_DeleteById.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_DeleteById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_DeleteById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_DeleteById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadById.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadById.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadById.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadById.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadByProviderId.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadByProviderId.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_ReadByProviderId.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_ReadByProviderId.sql diff --git a/src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Update.sql b/src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Update.sql similarity index 100% rename from src/Sql/Billing/dbo/Stored Procedures/ProviderPlan_Update.sql rename to src/Sql/dbo/Billing/Stored Procedures/ProviderPlan_Update.sql diff --git a/src/Sql/Billing/dbo/Tables/ClientOrganizationMigrationRecord.sql b/src/Sql/dbo/Billing/Tables/ClientOrganizationMigrationRecord.sql similarity index 100% rename from src/Sql/Billing/dbo/Tables/ClientOrganizationMigrationRecord.sql rename to src/Sql/dbo/Billing/Tables/ClientOrganizationMigrationRecord.sql diff --git a/src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql b/src/Sql/dbo/Billing/Tables/OrganizationInstallation.sql similarity index 100% rename from src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql rename to src/Sql/dbo/Billing/Tables/OrganizationInstallation.sql diff --git a/src/Sql/Billing/dbo/Tables/ProviderInvoiceItem.sql b/src/Sql/dbo/Billing/Tables/ProviderInvoiceItem.sql similarity index 100% rename from src/Sql/Billing/dbo/Tables/ProviderInvoiceItem.sql rename to src/Sql/dbo/Billing/Tables/ProviderInvoiceItem.sql diff --git a/src/Sql/Billing/dbo/Tables/ProviderPlan.sql b/src/Sql/dbo/Billing/Tables/ProviderPlan.sql similarity index 100% rename from src/Sql/Billing/dbo/Tables/ProviderPlan.sql rename to src/Sql/dbo/Billing/Tables/ProviderPlan.sql diff --git a/src/Sql/Billing/dbo/Views/ClientOrganizationMigrationRecordView.sql b/src/Sql/dbo/Billing/Views/ClientOrganizationMigrationRecordView.sql similarity index 100% rename from src/Sql/Billing/dbo/Views/ClientOrganizationMigrationRecordView.sql rename to src/Sql/dbo/Billing/Views/ClientOrganizationMigrationRecordView.sql diff --git a/src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql b/src/Sql/dbo/Billing/Views/OrganizationInstallationView.sql similarity index 100% rename from src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql rename to src/Sql/dbo/Billing/Views/OrganizationInstallationView.sql diff --git a/src/Sql/Billing/dbo/Views/ProviderInvoiceItemView.sql b/src/Sql/dbo/Billing/Views/ProviderInvoiceItemView.sql similarity index 100% rename from src/Sql/Billing/dbo/Views/ProviderInvoiceItemView.sql rename to src/Sql/dbo/Billing/Views/ProviderInvoiceItemView.sql diff --git a/src/Sql/Billing/dbo/Views/ProviderPlanView.sql b/src/Sql/dbo/Billing/Views/ProviderPlanView.sql similarity index 100% rename from src/Sql/Billing/dbo/Views/ProviderPlanView.sql rename to src/Sql/dbo/Billing/Views/ProviderPlanView.sql diff --git a/src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserAsymmetricKeys_Regenerate.sql similarity index 100% rename from src/Sql/KeyManagement/dbo/Stored Procedures/UserAsymmetricKeys_Regenerate.sql rename to src/Sql/dbo/KeyManagement/Stored Procedures/UserAsymmetricKeys_Regenerate.sql diff --git a/src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql rename to src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_ReadByUserId.sql diff --git a/src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql rename to src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql diff --git a/src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql similarity index 100% rename from src/Sql/dbo/Stored Procedures/UserSignatureKeyPair_SetForRotation.sql rename to src/Sql/dbo/KeyManagement/Stored Procedures/UserSignatureKeyPair_UpdateForRotation.sql diff --git a/src/Sql/dbo/Tables/UserSignatureKeyPair.sql b/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql similarity index 84% rename from src/Sql/dbo/Tables/UserSignatureKeyPair.sql rename to src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql index 9ee71a853b..e3b8d13474 100644 --- a/src/Sql/dbo/Tables/UserSignatureKeyPair.sql +++ b/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql @@ -9,3 +9,8 @@ CONSTRAINT [PK_UserSignatureKeyPair] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_UserSignatureKeyPair_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), ); +GO + +CREATE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId] + ON [dbo].[UserSignatureKeyPair]([UserId] ASC); +GO \ No newline at end of file diff --git a/src/Sql/dbo/Views/UserSignatureKeyPairView.sql b/src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql similarity index 100% rename from src/Sql/dbo/Views/UserSignatureKeyPairView.sql rename to src/Sql/dbo/KeyManagement/Views/UserSignatureKeyPairView.sql diff --git a/src/Sql/Platform/dbo/Stored Procedures/Installation_Create.sql b/src/Sql/dbo/Platform/Stored Procedures/Installation_Create.sql similarity index 100% rename from src/Sql/Platform/dbo/Stored Procedures/Installation_Create.sql rename to src/Sql/dbo/Platform/Stored Procedures/Installation_Create.sql diff --git a/src/Sql/Platform/dbo/Stored Procedures/Installation_DeleteById.sql b/src/Sql/dbo/Platform/Stored Procedures/Installation_DeleteById.sql similarity index 100% rename from src/Sql/Platform/dbo/Stored Procedures/Installation_DeleteById.sql rename to src/Sql/dbo/Platform/Stored Procedures/Installation_DeleteById.sql diff --git a/src/Sql/Platform/dbo/Stored Procedures/Installation_ReadById.sql b/src/Sql/dbo/Platform/Stored Procedures/Installation_ReadById.sql similarity index 100% rename from src/Sql/Platform/dbo/Stored Procedures/Installation_ReadById.sql rename to src/Sql/dbo/Platform/Stored Procedures/Installation_ReadById.sql diff --git a/src/Sql/Platform/dbo/Stored Procedures/Installation_Update.sql b/src/Sql/dbo/Platform/Stored Procedures/Installation_Update.sql similarity index 100% rename from src/Sql/Platform/dbo/Stored Procedures/Installation_Update.sql rename to src/Sql/dbo/Platform/Stored Procedures/Installation_Update.sql diff --git a/src/Sql/Platform/dbo/Tables/Installation.sql b/src/Sql/dbo/Platform/Tables/Installation.sql similarity index 100% rename from src/Sql/Platform/dbo/Tables/Installation.sql rename to src/Sql/dbo/Platform/Tables/Installation.sql diff --git a/src/Sql/Platform/dbo/Views/InstallationView.sql b/src/Sql/dbo/Platform/Views/InstallationView.sql similarity index 100% rename from src/Sql/Platform/dbo/Views/InstallationView.sql rename to src/Sql/dbo/Platform/Views/InstallationView.sql diff --git a/src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKeyDetails_ReadById.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKeyDetails_ReadById.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKeyDetails_ReadById.sql rename to src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKeyDetails_ReadById.sql diff --git a/src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_Create.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_Create.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_Create.sql rename to src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_Create.sql diff --git a/src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_DeleteByIds.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_DeleteByIds.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_DeleteByIds.sql rename to src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_DeleteByIds.sql diff --git a/src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_ReadByServiceAccountId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_ReadByServiceAccountId.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Stored Procedures/ApiKey/ApiKey_ReadByServiceAccountId.sql rename to src/Sql/dbo/SecretsManager/Stored Procedures/ApiKey/ApiKey_ReadByServiceAccountId.sql diff --git a/src/Sql/SecretsManager/dbo/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql b/src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql rename to src/Sql/dbo/SecretsManager/Stored Procedures/Event/Event_ReadPageByOrganizationIdServiceAccountId.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/AccessPolicy.sql b/src/Sql/dbo/SecretsManager/Tables/AccessPolicy.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/AccessPolicy.sql rename to src/Sql/dbo/SecretsManager/Tables/AccessPolicy.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/ApiKey.sql b/src/Sql/dbo/SecretsManager/Tables/ApiKey.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/ApiKey.sql rename to src/Sql/dbo/SecretsManager/Tables/ApiKey.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/Project.sql b/src/Sql/dbo/SecretsManager/Tables/Project.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/Project.sql rename to src/Sql/dbo/SecretsManager/Tables/Project.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/ProjectSecret.sql b/src/Sql/dbo/SecretsManager/Tables/ProjectSecret.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/ProjectSecret.sql rename to src/Sql/dbo/SecretsManager/Tables/ProjectSecret.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/Secret.sql b/src/Sql/dbo/SecretsManager/Tables/Secret.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/Secret.sql rename to src/Sql/dbo/SecretsManager/Tables/Secret.sql diff --git a/src/Sql/SecretsManager/dbo/Tables/ServiceAccount.sql b/src/Sql/dbo/SecretsManager/Tables/ServiceAccount.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Tables/ServiceAccount.sql rename to src/Sql/dbo/SecretsManager/Tables/ServiceAccount.sql diff --git a/src/Sql/SecretsManager/dbo/Views/ApiKeyDetailsView.sql b/src/Sql/dbo/SecretsManager/Views/ApiKeyDetailsView.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Views/ApiKeyDetailsView.sql rename to src/Sql/dbo/SecretsManager/Views/ApiKeyDetailsView.sql diff --git a/src/Sql/SecretsManager/dbo/Views/ApiKeyView.sql b/src/Sql/dbo/SecretsManager/Views/ApiKeyView.sql similarity index 100% rename from src/Sql/SecretsManager/dbo/Views/ApiKeyView.sql rename to src/Sql/dbo/SecretsManager/Views/ApiKeyView.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_Create.sql b/src/Sql/dbo/Stored Procedures/NotificationStatus_Create.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_Create.sql rename to src/Sql/dbo/Stored Procedures/NotificationStatus_Create.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_ReadByNotificationIdAndUserId.sql b/src/Sql/dbo/Stored Procedures/NotificationStatus_ReadByNotificationIdAndUserId.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_ReadByNotificationIdAndUserId.sql rename to src/Sql/dbo/Stored Procedures/NotificationStatus_ReadByNotificationIdAndUserId.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_Update.sql b/src/Sql/dbo/Stored Procedures/NotificationStatus_Update.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/NotificationStatus_Update.sql rename to src/Sql/dbo/Stored Procedures/NotificationStatus_Update.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_Create.sql b/src/Sql/dbo/Stored Procedures/Notification_Create.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_Create.sql rename to src/Sql/dbo/Stored Procedures/Notification_Create.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadById.sql b/src/Sql/dbo/Stored Procedures/Notification_ReadById.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadById.sql rename to src/Sql/dbo/Stored Procedures/Notification_ReadById.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql b/src/Sql/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql rename to src/Sql/dbo/Stored Procedures/Notification_ReadByUserIdAndStatus.sql diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_Update.sql b/src/Sql/dbo/Stored Procedures/Notification_Update.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_Update.sql rename to src/Sql/dbo/Stored Procedures/Notification_Update.sql diff --git a/src/Sql/NotificationCenter/dbo/Tables/Notification.sql b/src/Sql/dbo/Tables/Notification.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Tables/Notification.sql rename to src/Sql/dbo/Tables/Notification.sql diff --git a/src/Sql/NotificationCenter/dbo/Tables/NotificationStatus.sql b/src/Sql/dbo/Tables/NotificationStatus.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Tables/NotificationStatus.sql rename to src/Sql/dbo/Tables/NotificationStatus.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_Create.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_Create.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_DeleteById.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_DeleteById.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_DeleteById.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_DeleteById.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_ReadByDeletionDateBefore.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_ReadByDeletionDateBefore.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_ReadByDeletionDateBefore.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_ReadByDeletionDateBefore.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_ReadById.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_ReadById.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_ReadById.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_ReadById.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_ReadByUserId.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_ReadByUserId.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_ReadByUserId.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_ReadByUserId.sql diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql b/src/Sql/dbo/Tools/Stored Procedures/Send_Update.sql similarity index 100% rename from src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql rename to src/Sql/dbo/Tools/Stored Procedures/Send_Update.sql diff --git a/src/Sql/Tools/dbo/Tables/Send.sql b/src/Sql/dbo/Tools/Tables/Send.sql similarity index 100% rename from src/Sql/Tools/dbo/Tables/Send.sql rename to src/Sql/dbo/Tools/Tables/Send.sql diff --git a/src/Sql/Tools/dbo/Views/SendView.sql b/src/Sql/dbo/Tools/Views/SendView.sql similarity index 100% rename from src/Sql/Tools/dbo/Views/SendView.sql rename to src/Sql/dbo/Tools/Views/SendView.sql diff --git a/src/Sql/Vault/dbo/Functions/CipherDetails.sql b/src/Sql/dbo/Vault/Functions/CipherDetails.sql similarity index 100% rename from src/Sql/Vault/dbo/Functions/CipherDetails.sql rename to src/Sql/dbo/Vault/Functions/CipherDetails.sql diff --git a/src/Sql/Vault/dbo/Functions/UserCipherDetails.sql b/src/Sql/dbo/Vault/Functions/UserCipherDetails.sql similarity index 100% rename from src/Sql/Vault/dbo/Functions/UserCipherDetails.sql rename to src/Sql/dbo/Vault/Functions/UserCipherDetails.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Create.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Create.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_CreateWithCollections.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByIdUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadByUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadByUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadWithoutOrganizationsByUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadWithoutOrganizationsByUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_ReadWithoutOrganizationsByUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_ReadWithoutOrganizationsByUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherDetails_Update.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadById.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadById.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadByOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadByOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadByOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadUnassignedByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadUnassignedByOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationDetails_ReadUnassignedByOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationDetails_ReadUnassignedByOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Create.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_CreateWithCollections.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Delete.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Delete.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Delete.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Delete.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteById.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteById.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByIdsOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByIdsOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByIdsOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByIdsOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteByUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteByUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteDeleted.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteDeleted.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteDeleted.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_DeleteDeleted.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Move.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Move.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Move.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Move.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadById.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadById.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadByOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadByOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadByOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadByOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadCanEditByIdUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadCanEditByIdUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_ReadCanEditByIdUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_ReadCanEditByIdUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Restore.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Restore.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Restore.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Restore.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_RestoreByIdsOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_RestoreByIdsOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_RestoreByIdsOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_RestoreByIdsOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDelete.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDelete.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDelete.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDelete.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDeleteByIdsOrganizationId.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDeleteByIdsOrganizationId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_SoftDeleteByIdsOrganizationId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_SoftDeleteByIdsOrganizationId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_Update.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateCollections.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateCollections.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateCollections.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdatePartial.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdatePartial.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdatePartial.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdatePartial.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/Cipher_UpdateWithCollections.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql b/src/Sql/dbo/Vault/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql rename to src/Sql/dbo/Vault/Stored Procedures/Cipher/UserSecurityTasks_GetManyByCipherIds.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/CollectionCipher/CollectionCipher_AddCollectionsForManyCiphers.sql b/src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_AddCollectionsForManyCiphers.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/CollectionCipher/CollectionCipher_AddCollectionsForManyCiphers.sql rename to src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_AddCollectionsForManyCiphers.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/CollectionCipher/CollectionCipher_RemoveCollectionsFromManyCiphers.sql b/src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_RemoveCollectionsFromManyCiphers.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/CollectionCipher/CollectionCipher_RemoveCollectionsFromManyCiphers.sql rename to src/Sql/dbo/Vault/Stored Procedures/CollectionCipher/CollectionCipher_RemoveCollectionsFromManyCiphers.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql rename to src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql rename to src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Create.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_Create.sql rename to src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Create.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_DeleteById.sql b/src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_DeleteById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_DeleteById.sql rename to src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_DeleteById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_ReadById.sql b/src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_ReadById.sql rename to src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_ReadByUserId.sql b/src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadByUserId.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_ReadByUserId.sql rename to src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_ReadByUserId.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Update.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/Folder/Folder_Update.sql rename to src/Sql/dbo/Vault/Stored Procedures/Folder/Folder_Update.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Create.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Create.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Create.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadById.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_ReadByUserIdStatus.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql b/src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Update.sql similarity index 100% rename from src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_Update.sql rename to src/Sql/dbo/Vault/Stored Procedures/SecurityTask/SecurityTask_Update.sql diff --git a/src/Sql/Vault/dbo/Tables/Cipher.sql b/src/Sql/dbo/Vault/Tables/Cipher.sql similarity index 100% rename from src/Sql/Vault/dbo/Tables/Cipher.sql rename to src/Sql/dbo/Vault/Tables/Cipher.sql diff --git a/src/Sql/Vault/dbo/Tables/Folder.sql b/src/Sql/dbo/Vault/Tables/Folder.sql similarity index 100% rename from src/Sql/Vault/dbo/Tables/Folder.sql rename to src/Sql/dbo/Vault/Tables/Folder.sql diff --git a/src/Sql/Vault/dbo/Tables/SecurityTask.sql b/src/Sql/dbo/Vault/Tables/SecurityTask.sql similarity index 100% rename from src/Sql/Vault/dbo/Tables/SecurityTask.sql rename to src/Sql/dbo/Vault/Tables/SecurityTask.sql diff --git a/src/Sql/Vault/dbo/Views/CipherView.sql b/src/Sql/dbo/Vault/Views/CipherView.sql similarity index 100% rename from src/Sql/Vault/dbo/Views/CipherView.sql rename to src/Sql/dbo/Vault/Views/CipherView.sql diff --git a/src/Sql/Vault/dbo/Views/FolderView.sql b/src/Sql/dbo/Vault/Views/FolderView.sql similarity index 100% rename from src/Sql/Vault/dbo/Views/FolderView.sql rename to src/Sql/dbo/Vault/Views/FolderView.sql diff --git a/src/Sql/Vault/dbo/Views/SecurityTaskView.sql b/src/Sql/dbo/Vault/Views/SecurityTaskView.sql similarity index 100% rename from src/Sql/Vault/dbo/Views/SecurityTaskView.sql rename to src/Sql/dbo/Vault/Views/SecurityTaskView.sql diff --git a/src/Sql/NotificationCenter/dbo/Views/NotificationStatusDetailsView.sql b/src/Sql/dbo/Views/NotificationStatusDetailsView.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Views/NotificationStatusDetailsView.sql rename to src/Sql/dbo/Views/NotificationStatusDetailsView.sql diff --git a/src/Sql/NotificationCenter/dbo/Views/NotificationStatusView.sql b/src/Sql/dbo/Views/NotificationStatusView.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Views/NotificationStatusView.sql rename to src/Sql/dbo/Views/NotificationStatusView.sql diff --git a/src/Sql/NotificationCenter/dbo/Views/NotificationView.sql b/src/Sql/dbo/Views/NotificationView.sql similarity index 100% rename from src/Sql/NotificationCenter/dbo/Views/NotificationView.sql rename to src/Sql/dbo/Views/NotificationView.sql diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index d2775762e8..05b1aa5a4d 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -175,7 +175,7 @@ public class AccountsKeyManagementControllerTests } catch (BadRequestException ex) { - Assert.NotEmpty(ex.ModelState.Values); + Assert.NotEmpty(ex.ModelState!.Values); } } @@ -210,7 +210,7 @@ public class AccountsKeyManagementControllerTests var badRequestException = await Assert.ThrowsAsync(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data)); - Assert.Equal(1, badRequestException.ModelState.ErrorCount); + Assert.Equal(1, badRequestException.ModelState!.ErrorCount); Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage); await sutProvider.GetDependency().Received(1) .SetKeyConnectorKeyAsync(Arg.Do(user => @@ -284,7 +284,7 @@ public class AccountsKeyManagementControllerTests var badRequestException = await Assert.ThrowsAsync(() => sutProvider.Sut.PostConvertToKeyConnectorAsync()); - Assert.Equal(1, badRequestException.ModelState.ErrorCount); + Assert.Equal(1, badRequestException.ModelState!.ErrorCount); Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage); await sutProvider.GetDependency().Received(1) .ConvertToKeyConnectorAsync(Arg.Is(expectedUser)); diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 14f795e571..bca6bbc048 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Text.Json; using Bit.Api.Vault.Controllers; +using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; @@ -1774,16 +1775,16 @@ public class CiphersControllerTests Id = Guid.NewGuid(), UserId = userId, OrganizationId = organizationId, - Type = CipherType.SecureNote, - Data = JsonSerializer.Serialize(new CipherSecureNoteData()), + Type = CipherType.Login, + Data = JsonSerializer.Serialize(new CipherLoginData()), RevisionDate = oldDate2 }; var preloadedDetails = new List { detail1, detail2 }; var newDate1 = oldDate1.AddMinutes(5); var newDate2 = oldDate2.AddMinutes(5); - var updatedCipher1 = new Cipher { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data }; - var updatedCipher2 = new Cipher { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data }; + var updatedCipher1 = new CipherDetails { Id = detail1.Id, RevisionDate = newDate1, Type = detail1.Type, Data = detail1.Data }; + var updatedCipher2 = new CipherDetails { Id = detail2.Id, RevisionDate = newDate2, Type = detail2.Type, Data = detail2.Data }; sutProvider.GetDependency() .OrganizationUser(organizationId) @@ -1801,19 +1802,39 @@ public class CiphersControllerTests sutProvider.GetDependency() .ShareManyAsync( - Arg.Any>(), + Arg.Any>(), organizationId, Arg.Any>(), userId ) - .Returns(Task.FromResult>(new[] { updatedCipher1, updatedCipher2 })); + .Returns(Task.FromResult>(new[] { updatedCipher1, updatedCipher2 })); - var cipherRequests = preloadedDetails.Select(d => new CipherWithIdRequestModel + var cipherRequests = preloadedDetails.Select(d => { - Id = d.Id, - OrganizationId = d.OrganizationId!.Value.ToString(), - LastKnownRevisionDate = d.RevisionDate, - Type = d.Type + var m = new CipherWithIdRequestModel + { + Id = d.Id, + OrganizationId = d.OrganizationId!.Value.ToString(), + LastKnownRevisionDate = d.RevisionDate, + Type = d.Type, + }; + + if (d.Type == CipherType.Login) + { + m.Login = new CipherLoginModel + { + Username = "", + Password = "", + Uris = [], + }; + m.Name = ""; + m.Notes = ""; + m.Fields = Array.Empty(); + m.PasswordHistory = Array.Empty(); + } + + // similar for SecureNote, Card, etc., if you ever hit those branches + return m; }).ToList(); var model = new CipherBulkShareRequestModel @@ -1824,15 +1845,15 @@ public class CiphersControllerTests var result = await sutProvider.Sut.PutShareMany(model); - Assert.Equal(2, result.Length); - var revisionDates = result.Select(r => r.RevisionDate).ToList(); + Assert.Equal(2, result.Data.Count()); + var revisionDates = result.Data.Select(x => x.RevisionDate).ToList(); Assert.Contains(newDate1, revisionDates); Assert.Contains(newDate2, revisionDates); await sutProvider.GetDependency() .Received(1) .ShareManyAsync( - Arg.Is>(list => + Arg.Is>(list => list.Select(x => x.Item1.Id).OrderBy(id => id) .SequenceEqual(new[] { detail1.Id, detail2.Id }.OrderBy(id => id)) ), diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs index 0946841347..6ed84717de 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -7,12 +7,17 @@ namespace Bit.Core.Test.Models.Data.Integrations; public class IntegrationMessageTests { + private const string _messageId = "TestMessageId"; + [Fact] public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate() { var message = new IntegrationMessage { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = _messageId, RetryCount = 2, + RenderedTemplate = string.Empty, DelayUntilDate = null }; @@ -30,19 +35,22 @@ public class IntegrationMessageTests var message = new IntegrationMessage { Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = _messageId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, RetryCount = 2, - DelayUntilDate = null + DelayUntilDate = DateTime.UtcNow }; var json = message.ToJson(); var result = IntegrationMessage.FromJson(json); Assert.Equal(message.Configuration, result.Configuration); + Assert.Equal(message.MessageId, result.MessageId); Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); Assert.Equal(message.IntegrationType, result.IntegrationType); Assert.Equal(message.RetryCount, result.RetryCount); + Assert.Equal(message.DelayUntilDate, result.DelayUntilDate); } [Fact] @@ -51,4 +59,26 @@ public class IntegrationMessageTests var json = "{ Invalid JSON"; Assert.Throws(() => IntegrationMessage.FromJson(json)); } + + [Fact] + public void ToJson_BaseIntegrationMessage_DeserializesCorrectly() + { + var message = new IntegrationMessage + { + MessageId = _messageId, + RenderedTemplate = "This is the message", + IntegrationType = IntegrationType.Webhook, + RetryCount = 2, + DelayUntilDate = DateTime.UtcNow + }; + + var json = message.ToJson(); + var result = JsonSerializer.Deserialize(json); + + Assert.Equal(message.MessageId, result.MessageId); + Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); + Assert.Equal(message.IntegrationType, result.IntegrationType); + Assert.Equal(message.RetryCount, result.RetryCount); + Assert.Equal(message.DelayUntilDate, result.DelayUntilDate); + } } diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs new file mode 100644 index 0000000000..13704817ca --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusEventListenerServiceTests.cs @@ -0,0 +1,133 @@ +using System.Text.Json; +using Azure.Messaging.ServiceBus; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class AzureServiceBusEventListenerServiceTests +{ + private readonly IEventMessageHandler _handler = Substitute.For(); + private readonly ILogger _logger = + Substitute.For>(); + private const string _messageId = "messageId"; + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_logger) + .SetDependency("test-subscription", "subscriptionName") + .Create(); + } + + [Theory, BitAutoData] + public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) + { + var sutProvider = GetSutProvider(); + + await sutProvider.Sut.ProcessErrorAsync(args); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync(string.Empty, _messageId); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync("{ Inavlid JSON }", _messageId); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + "{ \"not a valid\", \"list of event messages\" }", + _messageId + ); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError() + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(DateTime.UtcNow), // wrong object - not EventMessage + _messageId + ); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(message), + _messageId + ); + + await sutProvider.GetDependency().Received(1).HandleEventAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" }))); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable messages) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessReceivedMessageAsync( + JsonSerializer.Serialize(messages), + _messageId + ); + + await sutProvider.GetDependency().Received(1).HandleManyEventsAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" }))); + } +} diff --git a/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs new file mode 100644 index 0000000000..b1eb117cf0 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/AzureServiceBusIntegrationListenerServiceTests.cs @@ -0,0 +1,124 @@ +#nullable enable + +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class AzureServiceBusIntegrationListenerServiceTests +{ + private const int _maxRetries = 3; + private const string _topicName = "test_topic"; + private const string _subscriptionName = "test_subscription"; + private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly IAzureServiceBusService _serviceBusService = Substitute.For(); + private readonly ILogger _logger = + Substitute.For>(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_serviceBusService) + .SetDependency(_topicName, "topicName") + .SetDependency(_subscriptionName, "subscriptionName") + .SetDependency(_maxRetries, "maxRetries") + .SetDependency(_logger) + .Create(); + } + + [Theory, BitAutoData] + public async Task ProcessErrorAsync_LogsError(ProcessErrorEventArgs args) + { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.ProcessErrorAsync(args); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + + var result = new IntegrationHandlerResult(false, message); + result.Retryable = false; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = _maxRetries; + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.False(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.Received(1).PublishToRetryAsync(message); + } + + [Theory, BitAutoData] + public async Task HandleMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + var result = new IntegrationHandlerResult(true, message); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = (IntegrationMessage)IntegrationMessage.FromJson(message.ToJson())!; + + Assert.True(await sutProvider.Sut.HandleMessageAsync(message.ToJson())); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + await _serviceBusService.DidNotReceiveWithAnyArgs().PublishToRetryAsync(default); + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs new file mode 100644 index 0000000000..9369690d86 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationEventWriteServiceTests.cs @@ -0,0 +1,57 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventIntegrationEventWriteServiceTests +{ + private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); + private readonly EventIntegrationEventWriteService Subject; + + public EventIntegrationEventWriteServiceTests() + { + Subject = new EventIntegrationEventWriteService(_eventIntegrationPublisher); + } + + [Theory, BitAutoData] + public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage) + { + var expected = JsonSerializer.Serialize(eventMessage); + await Subject.CreateAsync(eventMessage); + await _eventIntegrationPublisher.Received(1).PublishEventAsync( + Arg.Is(body => AssertJsonStringsMatch(eventMessage, body))); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable eventMessages) + { + await Subject.CreateManyAsync(eventMessages); + await _eventIntegrationPublisher.Received(1).PublishEventAsync( + Arg.Is(body => AssertJsonStringsMatch(eventMessages, body))); + } + + private static bool AssertJsonStringsMatch(EventMessage expected, string body) + { + var actual = JsonSerializer.Deserialize(body); + AssertHelper.AssertPropertyEqual(expected, actual, new[] { "IdempotencyId" }); + return true; + } + + private static bool AssertJsonStringsMatch(IEnumerable expected, string body) + { + using var actual = JsonSerializer.Deserialize>(body).GetEnumerator(); + + foreach (var expectedMessage in expected) + { + actual.MoveNext(); + AssertHelper.AssertPropertyEqual(expectedMessage, actual.Current, new[] { "IdempotencyId" }); + } + return true; + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index f0a0d1d724..0962df52cd 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -24,7 +24,7 @@ public class EventIntegrationHandlerTests private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; private const string _url = "https://localhost"; private const string _url2 = "https://example.com"; - private readonly IIntegrationPublisher _integrationPublisher = Substitute.For(); + private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For(); private SutProvider> GetSutProvider( List configurations) @@ -35,7 +35,7 @@ public class EventIntegrationHandlerTests return new SutProvider>() .SetDependency(configurationRepository) - .SetDependency(_integrationPublisher) + .SetDependency(_eventIntegrationPublisher) .SetDependency(IntegrationType.Webhook) .Create(); } @@ -45,6 +45,7 @@ public class EventIntegrationHandlerTests return new IntegrationMessage() { IntegrationType = IntegrationType.Webhook, + MessageId = "TestMessageId", Configuration = new WebhookIntegrationConfigurationDetails(_url), RenderedTemplate = template, RetryCount = 0, @@ -87,7 +88,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(NoConfigurations()); await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Empty(_integrationPublisher.ReceivedCalls()); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] @@ -101,8 +102,9 @@ public class EventIntegrationHandlerTests $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); } @@ -120,8 +122,9 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); } @@ -136,12 +139,13 @@ public class EventIntegrationHandlerTests sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Single(_integrationPublisher.ReceivedCalls()); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); } @@ -159,8 +163,9 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); - Assert.Single(_integrationPublisher.ReceivedCalls()); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); } @@ -171,7 +176,7 @@ public class EventIntegrationHandlerTests var sutProvider = GetSutProvider(NoConfigurations()); await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - Assert.Empty(_integrationPublisher.ReceivedCalls()); + Assert.Empty(_eventIntegrationPublisher.ReceivedCalls()); } [Theory, BitAutoData] @@ -186,7 +191,8 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } } @@ -203,10 +209,12 @@ public class EventIntegrationHandlerTests var expectedMessage = EventIntegrationHandlerTests.expectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); - await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); } } } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index 10f39665d5..10e42c92cc 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -15,6 +15,7 @@ public class IntegrationHandlerTests var expected = new IntegrationMessage() { Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + MessageId = "TestMessageId", IntegrationType = IntegrationType.Webhook, RenderedTemplate = "Template", DelayUntilDate = null, diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs new file mode 100644 index 0000000000..8fd7e460be --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/RabbitMqEventListenerServiceTests.cs @@ -0,0 +1,173 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.Extensions.Logging; +using NSubstitute; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class RabbitMqEventListenerServiceTests +{ + private const string _queueName = "test_queue"; + private readonly IRabbitMqService _rabbitMqService = Substitute.For(); + private readonly ILogger _logger = Substitute.For>(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_rabbitMqService) + .SetDependency(_logger) + .SetDependency(_queueName, "queueName") + .Create(); + } + + [Fact] + public async Task StartAsync_CreatesQueue() + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + await _rabbitMqService.Received(1).CreateEventQueueAsync( + Arg.Is(_queueName), + Arg.Is(cancellationToken) + ); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_EmptyJson_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: new byte[0]); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJson_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes("{ Inavlid JSON")); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Is(o => o.ToString().Contains("Invalid JSON")), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonArray_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(new[] { "not a valid", "list of event messages" })); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Fact] + public async Task ProcessReceivedMessageAsync_InvalidJsonObject_LogsError() + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(DateTime.UtcNow)); // wrong object - not EventMessage + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + _logger.Received(1).Log( + LogLevel.Error, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>()); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SingleEvent_DelegatesToHandler(EventMessage message) + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(message)); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + await sutProvider.GetDependency().Received(1).HandleEventAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(message, new[] { "IdempotencyId" }))); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_ManyEvents_DelegatesToHandler(IEnumerable messages) + { + var sutProvider = GetSutProvider(); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: JsonSerializer.SerializeToUtf8Bytes(messages)); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs); + + await sutProvider.GetDependency().Received(1).HandleManyEventsAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(messages, new[] { "IdempotencyId" }))); + } +} diff --git a/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs new file mode 100644 index 0000000000..92a51e1831 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/RabbitMqIntegrationListenerServiceTests.cs @@ -0,0 +1,230 @@ +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class RabbitMqIntegrationListenerServiceTests +{ + private const int _maxRetries = 3; + private const string _queueName = "test_queue"; + private const string _retryQueueName = "test_queue_retry"; + private const string _routingKey = "test_routing_key"; + private readonly IIntegrationHandler _handler = Substitute.For(); + private readonly IRabbitMqService _rabbitMqService = Substitute.For(); + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_handler) + .SetDependency(_rabbitMqService) + .SetDependency(_queueName, "queueName") + .SetDependency(_retryQueueName, "retryQueueName") + .SetDependency(_routingKey, "routingKey") + .SetDependency(_maxRetries, "maxRetries") + .Create(); + } + + [Fact] + public async Task StartAsync_CreatesQueues() + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + await _rabbitMqService.Received(1).CreateIntegrationQueuesAsync( + Arg.Is(_queueName), + Arg.Is(_retryQueueName), + Arg.Is(_routingKey), + Arg.Is(cancellationToken) + ); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureNotRetryable_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = false; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + + await _rabbitMqService.Received(1).PublishToDeadLetterAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureRetryableButTooManyRetries_PublishesToDeadLetterQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = _maxRetries; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + expected.ApplyRetry(result.DelayUntilDate); + await _rabbitMqService.Received(1).PublishToDeadLetterAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_FailureRetryable_PublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + message.RetryCount = 0; + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(false, message); + result.Retryable = true; + result.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + _handler.HandleAsync(Arg.Any()).Returns(result); + + var expected = IntegrationMessage.FromJson(message.ToJson()); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(expected.ToJson())); + + expected.ApplyRetry(result.DelayUntilDate); + await _rabbitMqService.Received(1).PublishToRetryAsync( + Arg.Any(), + Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "DelayUntilDate" })), + Arg.Any()); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_SuccessfulResult_Succeeds(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(-1); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + var result = new IntegrationHandlerResult(true, message); + _handler.HandleAsync(Arg.Any()).Returns(result); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _handler.Received(1).HandleAsync(Arg.Is(message.ToJson())); + + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .RepublishToRetryQueueAsync(default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ProcessReceivedMessageAsync_TooEarlyRetry_RepublishesToRetryQueue(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + var cancellationToken = CancellationToken.None; + await sutProvider.Sut.StartAsync(cancellationToken); + + message.DelayUntilDate = DateTime.UtcNow.AddMinutes(1); + var eventArgs = new BasicDeliverEventArgs( + consumerTag: string.Empty, + deliveryTag: 0, + redelivered: true, + exchange: string.Empty, + routingKey: string.Empty, + new BasicProperties(), + body: Encoding.UTF8.GetBytes(message.ToJson()) + ); + + await sutProvider.Sut.ProcessReceivedMessageAsync(eventArgs, cancellationToken); + + await _rabbitMqService.Received(1) + .RepublishToRetryQueueAsync(Arg.Any(), Arg.Any()); + + await _handler.DidNotReceiveWithAnyArgs().HandleAsync(default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToRetryAsync(default, default, default); + await _rabbitMqService.DidNotReceiveWithAnyArgs() + .PublishToDeadLetterAsync(default, default, default); + } +} diff --git a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs index 93c0aa8dd4..92544551e0 100644 --- a/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackServiceTests.cs @@ -1,4 +1,6 @@ -using System.Net; +#nullable enable + +using System.Net; using System.Text.Json; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -257,10 +259,10 @@ public class SlackServiceTests public void GetRedirectUrl_ReturnsCorrectUrl() { var sutProvider = GetSutProvider(); - var ClientId = sutProvider.GetDependency().Slack.ClientId; - var Scopes = sutProvider.GetDependency().Slack.Scopes; + var clientId = sutProvider.GetDependency().Slack.ClientId; + var scopes = sutProvider.GetDependency().Slack.Scopes; var redirectUrl = "https://example.com/callback"; - var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={ClientId}&scope={Scopes}&redirect_uri={redirectUrl}"; + var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={clientId}&scope={scopes}&redirect_uri={redirectUrl}"; var result = sutProvider.Sut.GetRedirectUrl(redirectUrl); Assert.Equal(expectedUrl, result); } diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 79c7569ea3..7870f543d1 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -79,6 +79,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal("Too Many Requests", result.FailureReason); } [Theory, BitAutoData] @@ -99,6 +100,7 @@ public class WebhookIntegrationHandlerTests Assert.Equal(result.Message, message); Assert.True(result.DelayUntilDate.HasValue); Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + Assert.Equal("Too Many Requests", result.FailureReason); } [Theory, BitAutoData] @@ -117,6 +119,7 @@ public class WebhookIntegrationHandlerTests Assert.True(result.Retryable); Assert.Equal(result.Message, message); Assert.False(result.DelayUntilDate.HasValue); + Assert.Equal("Internal Server Error", result.FailureReason); } [Theory, BitAutoData] @@ -135,5 +138,6 @@ public class WebhookIntegrationHandlerTests Assert.False(result.Retryable); Assert.Equal(result.Message, message); Assert.Null(result.DelayUntilDate); + Assert.Equal("Temporary Redirect", result.FailureReason); } } diff --git a/test/Core.Test/Vault/Services/CipherServiceTests.cs b/test/Core.Test/Vault/Services/CipherServiceTests.cs index 061d90bcc3..95fd8179e3 100644 --- a/test/Core.Test/Vault/Services/CipherServiceTests.cs +++ b/test/Core.Test/Vault/Services/CipherServiceTests.cs @@ -72,7 +72,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_WrongRevisionDate_Throws(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId) .Returns(new Organization @@ -651,7 +651,7 @@ public class CipherServiceTests [BitAutoData("")] [BitAutoData("Correct Time")] public async Task ShareManyAsync_CorrectRevisionDate_Passes(string revisionDateString, - SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) + SutProvider sutProvider, IEnumerable ciphers, Organization organization, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organization.Id) .Returns(new Organization @@ -1173,7 +1173,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_FreeOrgWithAttachment_Throws(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId).Returns(new Organization { @@ -1194,7 +1194,7 @@ public class CipherServiceTests [Theory, BitAutoData] public async Task ShareManyAsync_PaidOrgWithAttachment_Passes(SutProvider sutProvider, - IEnumerable ciphers, Guid organizationId, List collectionIds) + IEnumerable ciphers, Guid organizationId, List collectionIds) { sutProvider.GetDependency().GetByIdAsync(organizationId) .Returns(new Organization diff --git a/util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql b/util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql index c90eb42f58..5310aa1015 100644 --- a/util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql +++ b/util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql @@ -11,6 +11,16 @@ CREATE TABLE [dbo].[UserSignatureKeyPair] ( ); GO +IF NOT EXISTS(SELECT name +FROM sys.indexes +WHERE name = 'IX_UserSignatureKeyPair_UserId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_UserSignatureKeyPair_UserId] + ON [dbo].[UserSignatureKeyPair]([UserId] ASC); +END +GO + + CREATE OR ALTER VIEW [dbo].[UserSignatureKeyPairView] AS SELECT