1
0
mirror of https://github.com/bitwarden/server synced 2026-01-08 11:33:26 +00:00

[PM-17562] Add HEC integration support (#6010)

* [PM-17562] Add HEC integration support

* Re-ordered parameters per PR suggestion

* Apply suggestions from code review

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

* Refactored webhook request model validation to be more clear

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
Brant DeBow
2025-07-01 08:52:38 -04:00
committed by GitHub
parent e8ad23c8bc
commit f6cd661e8e
22 changed files with 302 additions and 67 deletions

View File

@@ -14,7 +14,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RetryCount = 2,
RenderedTemplate = string.Empty,
@@ -34,7 +34,7 @@ public class IntegrationMessageTests
{
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = _messageId,
RenderedTemplate = "This is the message",
IntegrationType = IntegrationType.Webhook,

View File

@@ -22,6 +22,22 @@ public class OrganizationIntegrationConfigurationDetailsTests
Assert.Equal(expected, result.ToJsonString());
}
[Fact]
public void MergedConfiguration_WithSameKeyIndConfigAndIntegration_GivesPrecedenceToConfiguration()
{
var config = new { config = "A new config value" };
var integration = new { config = "An integration value" };
var expectedObj = new { config = "A new config value" };
var expected = JsonSerializer.Serialize(expectedObj);
var sut = new OrganizationIntegrationConfigurationDetails();
sut.Configuration = JsonSerializer.Serialize(config);
sut.IntegrationConfiguration = JsonSerializer.Serialize(integration);
var result = sut.MergedConfiguration;
Assert.Equal(expected, result.ToJsonString());
}
[Fact]
public void MergedConfiguration_WithInvalidJsonConfigAndIntegration_ReturnsEmptyJson()
{

View File

@@ -23,8 +23,8 @@ public class EventIntegrationHandlerTests
private const string _templateWithOrganization = "Org: #OrganizationName#";
private const string _templateWithUser = "#UserName#, #UserEmail#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
private const string _url = "https://localhost";
private const string _url2 = "https://example.com";
private static readonly Uri _uri = new Uri("https://localhost");
private static readonly Uri _uri2 = new Uri("https://example.com");
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =
Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();
@@ -50,7 +50,7 @@ public class EventIntegrationHandlerTests
{
IntegrationType = IntegrationType.Webhook,
MessageId = "TestMessageId",
Configuration = new WebhookIntegrationConfigurationDetails(_url),
Configuration = new WebhookIntegrationConfigurationDetails(_uri),
RenderedTemplate = template,
RetryCount = 0,
DelayUntilDate = null
@@ -66,7 +66,7 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
return [config];
@@ -76,11 +76,11 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 });
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri2 });
config2.Template = template;
return [config, config2];
@@ -90,7 +90,7 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = "Invalid Configuration!";
@@ -101,7 +101,7 @@ public class EventIntegrationHandlerTests
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { });
@@ -149,7 +149,7 @@ public class EventIntegrationHandlerTests
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2);
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
@@ -304,7 +304,7 @@ public class EventIntegrationHandlerTests
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2);
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
}

View File

@@ -14,7 +14,7 @@ public class IntegrationHandlerTests
var sut = new TestIntegrationHandler();
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
MessageId = "TestMessageId",
IntegrationType = IntegrationType.Webhook,
RenderedTemplate = "Template",

View File

@@ -19,7 +19,7 @@ public class WebhookIntegrationHandlerTests
private readonly HttpClient _httpClient;
private const string _scheme = "Bearer";
private const string _token = "AUTH_TOKEN";
private const string _webhookUrl = "http://localhost/test/event";
private static readonly Uri _webhookUri = new Uri("https://localhost");
public WebhookIntegrationHandlerTests()
{
@@ -45,7 +45,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri);
var result = await sutProvider.Sut.HandleAsync(message);
@@ -63,7 +63,7 @@ public class WebhookIntegrationHandlerTests
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Null(request.Headers.Authorization);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
Assert.Equal(_webhookUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
@@ -71,7 +71,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
var result = await sutProvider.Sut.HandleAsync(message);
@@ -89,7 +89,7 @@ public class WebhookIntegrationHandlerTests
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
Assert.Equal(_webhookUri, request.RequestUri);
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
}
@@ -101,7 +101,7 @@ public class WebhookIntegrationHandlerTests
var retryAfter = now.AddSeconds(60);
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
@@ -124,7 +124,7 @@ public class WebhookIntegrationHandlerTests
var sutProvider = GetSutProvider();
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
var retryAfter = now.AddSeconds(60);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TooManyRequests)
@@ -145,7 +145,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.InternalServerError)
@@ -164,7 +164,7 @@ public class WebhookIntegrationHandlerTests
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var sutProvider = GetSutProvider();
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUri, _scheme, _token);
_handler.Fallback
.WithStatusCode(HttpStatusCode.TemporaryRedirect)

View File

@@ -1,5 +1,6 @@
#nullable enable
using System.Text.Json;
using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -39,6 +40,16 @@ public class IntegrationTemplateProcessorTests
Assert.Equal(expected, result);
}
[Theory, BitAutoData]
public void ReplaceTokens_WithEventMessageToken_ReplacesWithSerializedJson(EventMessage eventMessage)
{
var template = "#EventMessage#";
var expected = $"{JsonSerializer.Serialize(eventMessage)}";
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
Assert.Equal(expected, result);
}
[Theory, BitAutoData]
public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage)
{