Event integration updates and cleanups (#6288)

* Event integration updates and cleanups

* Fix empty message on ArgumentException

* Adjust exception message

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
Brant DeBow 2025-09-08 10:54:43 -04:00 committed by GitHub
parent 7e50a46d3b
commit 0fbbb6a984
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 76 additions and 64 deletions

View File

@ -28,12 +28,12 @@ public abstract class EventLoggingListenerService : BackgroundService
if (root.ValueKind == JsonValueKind.Array) if (root.ValueKind == JsonValueKind.Array)
{ {
var eventMessages = root.Deserialize<IEnumerable<EventMessage>>(); var eventMessages = root.Deserialize<IEnumerable<EventMessage>>();
await _handler.HandleManyEventsAsync(eventMessages); await _handler.HandleManyEventsAsync(eventMessages ?? throw new JsonException("Deserialize returned null"));
} }
else if (root.ValueKind == JsonValueKind.Object) else if (root.ValueKind == JsonValueKind.Object)
{ {
var eventMessage = root.Deserialize<EventMessage>(); var eventMessage = root.Deserialize<EventMessage>();
await _handler.HandleEventAsync(eventMessage); await _handler.HandleEventAsync(eventMessage ?? throw new JsonException("Deserialize returned null"));
} }
else else
{ {

View File

@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below using Bit.Core.Models.Data;
#nullable disable
using Bit.Core.Models.Data;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -1,6 +1,5 @@
// FIXME: Update this file to be null safe and then delete the line below using System.Globalization;
#nullable disable using System.Net;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
namespace Bit.Core.Services; namespace Bit.Core.Services;
@ -20,8 +19,56 @@ public abstract class IntegrationHandlerBase<T> : IIntegrationHandler<T>
public async Task<IntegrationHandlerResult> HandleAsync(string json) public async Task<IntegrationHandlerResult> HandleAsync(string json)
{ {
var message = IntegrationMessage<T>.FromJson(json); var message = IntegrationMessage<T>.FromJson(json);
return await HandleAsync(message); return await HandleAsync(message ?? throw new ArgumentException("IntegrationMessage was null when created from the provided JSON"));
} }
public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message); public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
protected IntegrationHandlerResult ResultFromHttpResponse(
HttpResponseMessage response,
IntegrationMessage<T> message,
TimeProvider timeProvider)
{
var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message);
if (response.IsSuccessStatusCode) return result;
switch (response.StatusCode)
{
case HttpStatusCode.TooManyRequests:
case HttpStatusCode.RequestTimeout:
case HttpStatusCode.InternalServerError:
case HttpStatusCode.BadGateway:
case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.GatewayTimeout:
result.Retryable = true;
result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}";
if (response.Headers.TryGetValues("Retry-After", out var values))
{
var value = values.FirstOrDefault();
if (int.TryParse(value, out var seconds))
{
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
}
else if (DateTimeOffset.TryParseExact(value,
"r", // "r" is the round-trip format: RFC1123
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var retryDate))
{
// Retry-after was specified as a date. Adjust DelayUntilDate to the specified date.
result.DelayUntilDate = retryDate.UtcDateTime;
}
}
break;
default:
result.Retryable = false;
result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}";
break;
}
return result;
}
} }

View File

@ -418,13 +418,21 @@ dependencies and integrations. For instance, `SlackIntegrationHandler` needs a `
`AddEventIntegrationServices` has a call to `AddSlackService`. Same thing for webhooks when it `AddEventIntegrationServices` has a call to `AddSlackService`. Same thing for webhooks when it
comes to defining a custom HttpClient by name. comes to defining a custom HttpClient by name.
1. In `AddEventIntegrationServices` create the listener configuration: In `AddEventIntegrationServices`:
1. Create the singleton for the handler:
``` csharp
services.TryAddSingleton<IIntegrationHandler<ExampleIntegrationConfigurationDetails>, ExampleIntegrationHandler>();
```
2. Create the listener configuration:
``` csharp ``` csharp
var exampleConfiguration = new ExampleListenerConfiguration(globalSettings); var exampleConfiguration = new ExampleListenerConfiguration(globalSettings);
``` ```
2. Add the integration to both the RabbitMQ and ASB specific declarations: 3. Add the integration to both the RabbitMQ and ASB specific declarations:
``` csharp ``` csharp
services.AddRabbitMqIntegration<ExampleIntegrationConfigurationDetails, ExampleListenerConfiguration>(exampleConfiguration); services.AddRabbitMqIntegration<ExampleIntegrationConfigurationDetails, ExampleListenerConfiguration>(exampleConfiguration);

View File

@ -1,7 +1,5 @@
#nullable enable #nullable enable
using System.Globalization;
using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
@ -17,7 +15,8 @@ public class WebhookIntegrationHandler(
public const string HttpClientName = "WebhookIntegrationHandlerHttpClient"; public const string HttpClientName = "WebhookIntegrationHandlerHttpClient";
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<WebhookIntegrationConfigurationDetails> message) public override async Task<IntegrationHandlerResult> HandleAsync(
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{ {
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri); var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri);
request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
@ -28,45 +27,8 @@ public class WebhookIntegrationHandler(
parameter: message.Configuration.Token parameter: message.Configuration.Token
); );
} }
var response = await _httpClient.SendAsync(request); var response = await _httpClient.SendAsync(request);
var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); return ResultFromHttpResponse(response, message, timeProvider);
switch (response.StatusCode)
{
case HttpStatusCode.TooManyRequests:
case HttpStatusCode.RequestTimeout:
case HttpStatusCode.InternalServerError:
case HttpStatusCode.BadGateway:
case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.GatewayTimeout:
result.Retryable = true;
result.FailureReason = response.ReasonPhrase ?? $"Failure with status code: {(int)response.StatusCode}";
if (response.Headers.TryGetValues("Retry-After", out var values))
{
var value = values.FirstOrDefault();
if (int.TryParse(value, out var seconds))
{
// Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
result.DelayUntilDate = timeProvider.GetUtcNow().AddSeconds(seconds).UtcDateTime;
}
else if (DateTimeOffset.TryParseExact(value,
"r", // "r" is the round-trip format: RFC1123
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out var retryDate))
{
// Retry-after was specified as a date. Adjust DelayUntilDate to the specified date.
result.DelayUntilDate = retryDate.UtcDateTime;
}
}
break;
default:
result.Retryable = false;
result.FailureReason = response.ReasonPhrase ?? $"Failure with status code {(int)response.StatusCode}";
break;
}
return result;
} }
} }

View File

@ -1,13 +1,10 @@
// FIXME: Update this file to be null safe and then delete the line below using AutoMapper;
#nullable disable
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration
{ {
public virtual Organization Organization { get; set; } public virtual required Organization Organization { get; set; }
} }
public class OrganizationIntegrationMapperProfile : Profile public class OrganizationIntegrationMapperProfile : Profile

View File

@ -1,13 +1,10 @@
// FIXME: Update this file to be null safe and then delete the line below using AutoMapper;
#nullable disable
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models; namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration
{ {
public virtual OrganizationIntegration OrganizationIntegration { get; set; } public virtual required OrganizationIntegration OrganizationIntegration { get; set; }
} }
public class OrganizationIntegrationConfigurationMapperProfile : Profile public class OrganizationIntegrationConfigurationMapperProfile : Profile

View File

@ -51,6 +51,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success); Assert.True(result.Success);
Assert.Equal(result.Message, message); Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient( sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
@ -59,6 +60,7 @@ public class WebhookIntegrationHandlerTests
Assert.Single(_handler.CapturedRequests); Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0]; var request = _handler.CapturedRequests[0];
Assert.NotNull(request); Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync(); var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(HttpMethod.Post, request.Method);
@ -77,6 +79,7 @@ public class WebhookIntegrationHandlerTests
Assert.True(result.Success); Assert.True(result.Success);
Assert.Equal(result.Message, message); Assert.Equal(result.Message, message);
Assert.Empty(result.FailureReason);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient( sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
@ -85,6 +88,7 @@ public class WebhookIntegrationHandlerTests
Assert.Single(_handler.CapturedRequests); Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0]; var request = _handler.CapturedRequests[0];
Assert.NotNull(request); Assert.NotNull(request);
Assert.NotNull(request.Content);
var returned = await request.Content.ReadAsStringAsync(); var returned = await request.Content.ReadAsStringAsync();
Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal(HttpMethod.Post, request.Method);