1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-28 06:36:15 -05:00

[PM-17562] Add HEC integration support

This commit is contained in:
Brant DeBow 2025-06-26 15:30:06 -04:00
parent b951b38c37
commit de30df8897
No known key found for this signature in database
GPG Key ID: 94411BB25947C72B
22 changed files with 288 additions and 62 deletions

View File

@ -31,6 +31,9 @@
},
{
"Name": "events-webhook-subscription"
},
{
"Name": "events-hec-subscription"
}
]
},
@ -64,6 +67,20 @@
}
}
]
},
{
"Name": "integration-hec-subscription",
"Rules": [
{
"Name": "hec-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "hec"
}
}
}
]
}
]
}

View File

@ -27,6 +27,8 @@ public class OrganizationIntegrationConfigurationRequestModel
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<SlackIntegrationConfiguration>();
case IntegrationType.Webhook:
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<WebhookIntegrationConfiguration>();
case IntegrationType.Hec:
return !string.IsNullOrWhiteSpace(Template) && Configuration is null;
default:
return false;

View File

@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
#nullable enable
@ -39,10 +41,18 @@ public class OrganizationIntegrationRequestModel : IValidatableObject
yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) });
break;
case IntegrationType.Webhook:
if (Configuration is not null)
if (!string.IsNullOrWhiteSpace(Configuration) && !IsIntegrationValid<WebhookIntegration>())
{
yield return new ValidationResult(
"Webhook integrations must not include configuration.",
"Webhook integrations must include valid configuration.",
new[] { nameof(Configuration) });
}
break;
case IntegrationType.Hec:
if (!IsIntegrationValid<HecIntegration>())
{
yield return new ValidationResult(
"HEC integrations must include valid configuration.",
new[] { nameof(Configuration) });
}
break;
@ -53,4 +63,22 @@ public class OrganizationIntegrationRequestModel : IValidatableObject
break;
}
}
private bool IsIntegrationValid<T>()
{
if (string.IsNullOrWhiteSpace(Configuration))
{
return false;
}
try
{
var config = JsonSerializer.Deserialize<T>(Configuration);
return config is not null;
}
catch
{
return false;
}
}
}

View File

@ -6,6 +6,7 @@ public enum IntegrationType : int
Scim = 2,
Slack = 3,
Webhook = 4,
Hec = 5
}
public static class IntegrationTypeExtensions
@ -18,6 +19,8 @@ public static class IntegrationTypeExtensions
return "slack";
case IntegrationType.Webhook:
return "webhook";
case IntegrationType.Hec:
return "hec";
default:
throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}");
}

View File

@ -0,0 +1,5 @@
#nullable enable
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record HecIntegration(string Scheme, string Token, Uri Uri);

View File

@ -0,0 +1,5 @@
#nullable enable
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfiguration(string Url, string? Scheme = null, string? Token = null);
public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null);

View File

@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null);
public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null);

View File

@ -197,22 +197,37 @@ interface and therefore can also handle directly all the message publishing func
Organizations can configure integration configurations to send events to different endpoints -- each
handler maps to a specific integration and checks for the configuration when it receives an event.
Currently, there are integrations / handlers for Slack and webhooks (as mentioned above).
Currently, there are integrations / handlers for Slack, webhooks, and HTTP Event Collector (HEC).
### `OrganizationIntegration`
- The top-level object that enables a specific integration for the organization.
- Includes any properties that apply to the entire integration across all events.
- For Slack, it consists of the token: `{ "token": "xoxb-token-from-slack" }`
- For webhooks, it is `null`. However, even though there is no configuration, an organization must
have a webhook `OrganizationIntegration` to enable configuration via `OrganizationIntegrationConfiguration`.
- For Slack, it consists of the token: `{ "Token": "xoxb-token-from-slack" }`.
- For webhooks, it is optional. Webhooks can either be configured at this level or the configuration level,
but the configuration level takes precedence. However, even though it is optional, an organization must
have a webhook `OrganizationIntegration` (even will a `null` `Configuration`) to enable configuration
via `OrganizationIntegrationConfiguration`.
- For HEC, it consists of the scheme, token, and URI:
```json
{
"Scheme": "Bearer",
"Token": "Auth-token-from-HEC-service",
"Uri": "https://example.com/api"
}
```
### `OrganizationIntegrationConfiguration`
- This contains the configurations specific to each `EventType` for the integration.
- `Configuration` contains the event-specific configuration.
- For Slack, this would contain what channel to send the message to: `{ "channelId": "C123456" }`
- For Webhook, this is the URL the request should be sent to: `{ "url": "https://api.example.com" }`
- For webhooks, this is the URL the request should be sent to: `{ "url": "https://api.example.com" }`
- Optionally this also can include a `Scheme` and `Token` if this webhook needs Authentication.
- As stated above, all of this information can be specified here or at the `OrganizationIntegration`
level, but any properties declared here will take precedence over the ones above.
- for HEC, this must be null. HEC is configured only at the `OrganizationIntegration` level.
- `Template` contains a template string that is expected to be filled in with the contents of the actual event.
- The tokens in the string are wrapped in `#` characters. For instance, the UserId would be `#UserId#`.
- The `IntegrationTemplateProcessor` does the actual work of replacing these tokens with introspected values from
@ -225,6 +240,8 @@ Currently, there are integrations / handlers for Slack and webhooks (as mentione
- This is the combination of both the `OrganizationIntegration` and `OrganizationIntegrationConfiguration` into
a single object. The combined contents tell the integration's handler all the details needed to send to an
external service.
- `OrganizationIntegrationConfiguration` takes precedence over `OrganizationIntegration` - any keys present in
both will receive the value declared in `OrganizationIntegrationConfiguration`.
- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from
the database to determine what to publish at the integration level.

View File

@ -6,8 +6,6 @@ using System.Net.Http.Headers;
using System.Text;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
#nullable enable
namespace Bit.Core.Services;
public class WebhookIntegrationHandler(
@ -21,7 +19,7 @@ public class WebhookIntegrationHandler(
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
{
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url);
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Uri);
request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(message.Configuration.Scheme))
{

View File

@ -1,5 +1,6 @@
#nullable enable
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Bit.Core.AdminConsole.Utilities;
@ -19,8 +20,15 @@ public static partial class IntegrationTemplateProcessor
return TokenRegex().Replace(template, match =>
{
var propertyName = match.Groups[1].Value;
var property = type.GetProperty(propertyName);
return property?.GetValue(values)?.ToString() ?? match.Value;
if (propertyName == "EventMessage")
{
return JsonSerializer.Serialize(values);
}
else
{
var property = type.GetProperty(propertyName);
return property?.GetValue(values)?.ToString() ?? match.Value;
}
});
}

View File

@ -297,6 +297,8 @@ public class GlobalSettings : IGlobalSettings
public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription";
public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription";
public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription";
public virtual string HecEventSubscriptionName { get; set; } = "events-hec-subscription";
public virtual string HecIntegrationSubscriptionName { get; set; } = "integration-hec-subscription";
public string ConnectionString
{
@ -336,6 +338,9 @@ public class GlobalSettings : IGlobalSettings
public virtual string WebhookEventsQueueName { get; set; } = "events-webhook-queue";
public virtual string WebhookIntegrationQueueName { get; set; } = "integration-webhook-queue";
public virtual string WebhookIntegrationRetryQueueName { get; set; } = "integration-webhook-retry-queue";
public virtual string HecEventsQueueName { get; set; } = "events-hec-queue";
public virtual string HecIntegrationQueueName { get; set; } = "integration-hec-queue";
public virtual string HecIntegrationRetryQueueName { get; set; } = "integration-hec-retry-queue";
public string HostName
{

View File

@ -664,6 +664,13 @@ public static class ServiceCollectionExtensions
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName,
integrationType: IntegrationType.Webhook,
globalSettings: globalSettings);
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName,
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName,
integrationType: IntegrationType.Hec,
globalSettings: globalSettings);
return services;
}
@ -750,6 +757,13 @@ public static class ServiceCollectionExtensions
globalSettings.EventLogging.RabbitMq.MaxRetries,
IntegrationType.Webhook);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
globalSettings.EventLogging.RabbitMq.HecEventsQueueName,
globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName,
globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.MaxRetries,
IntegrationType.Hec);
return services;
}

View File

@ -188,7 +188,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -225,7 +225,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -387,7 +387,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = null;
@ -473,7 +473,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -515,7 +515,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
organizationIntegration.OrganizationId = organizationId;
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"));
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";
@ -555,7 +555,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
{
organizationIntegration.OrganizationId = organizationId;
organizationIntegration.Type = IntegrationType.Webhook;
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
var webhookConfig = new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost"), Scheme: "Bearer", Token: "AUTH-TOKEN");
model.Configuration = JsonSerializer.Serialize(webhookConfig);
model.Template = "Template String";

View File

@ -17,13 +17,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.CloudBillingSync));
Assert.False(condition: model.IsValidForType(IntegrationType.CloudBillingSync));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
@ -32,25 +32,55 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
var result = model.IsValidForType(IntegrationType.Slack);
Assert.False(result);
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyNonNullHecConfiguration_ReturnsFalse(string? config)
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
public void IsValidForType_NullHecConfiguration_ReturnsTrue()
{
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = null,
Template = "template"
};
Assert.True(condition: model.IsValidForType(IntegrationType.Hec));
}
[Theory]
[InlineData(data: null)]
[InlineData(data: "")]
[InlineData(data: " ")]
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = template
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
@ -62,7 +92,9 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Slack));
Assert.False(condition: model.IsValidForType(IntegrationType.Webhook));
Assert.False(condition: model.IsValidForType(IntegrationType.Hec));
}
[Fact]
@ -74,13 +106,13 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.False(model.IsValidForType(IntegrationType.Scim));
Assert.False(condition: model.IsValidForType(IntegrationType.Scim));
}
[Fact]
public void IsValidForType_ValidSlackConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
var config = JsonSerializer.Serialize(value: new SlackIntegrationConfiguration(ChannelId: "C12345"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
@ -88,33 +120,36 @@ public class OrganizationIntegrationConfigurationRequestModelTests
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Slack));
Assert.True(condition: model.IsValidForType(IntegrationType.Slack));
}
[Fact]
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(Uri: new Uri("https://localhost")));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
{
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
var config = JsonSerializer.Serialize(value: new WebhookIntegrationConfiguration(
Uri: new Uri("https://localhost"),
Scheme: "Bearer",
Token: "AUTH-TOKEN"));
var model = new OrganizationIntegrationConfigurationRequestModel
{
Configuration = config,
Template = "template"
};
Assert.True(model.IsValidForType(IntegrationType.Webhook));
Assert.True(condition: model.IsValidForType(IntegrationType.Webhook));
}
[Fact]
@ -128,6 +163,6 @@ public class OrganizationIntegrationConfigurationRequestModelTests
var unknownType = (IntegrationType)999;
Assert.False(model.IsValidForType(unknownType));
Assert.False(condition: model.IsValidForType(unknownType));
}
}

View File

@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
using Xunit;
@ -70,7 +72,7 @@ public class OrganizationIntegrationRequestModelTests
}
[Fact]
public void Validate_Webhook_WithConfiguration_ReturnsConfigurationError()
public void Validate_Webhook_WithInvalidConfiguration_ReturnsConfigurationError()
{
var model = new OrganizationIntegrationRequestModel
{
@ -82,7 +84,67 @@ public class OrganizationIntegrationRequestModelTests
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("must not include configuration", results[0].ErrorMessage);
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
}
[Fact]
public void Validate_Webhook_WithValidConfiguration_ReturnsNoErrors()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Webhook,
Configuration = JsonSerializer.Serialize(new WebhookIntegration(new Uri("https://example.com")))
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Empty(results);
}
[Fact]
public void Validate_Hec_WithNullConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = null
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
}
[Fact]
public void Validate_Hec_WithInvalidConfiguration_ReturnsError()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = "Not valid"
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Single(results);
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
Assert.Contains("must include valid configuration", results[0].ErrorMessage);
}
[Fact]
public void Validate_Hec_WithValidConfiguration_ReturnsNoErrors()
{
var model = new OrganizationIntegrationRequestModel
{
Type = IntegrationType.Hec,
Configuration = JsonSerializer.Serialize(new HecIntegration("Bearer", Token: "Token", Uri: new Uri("http://localhost")))
};
var results = model.Validate(new ValidationContext(model)).ToList();
Assert.Empty(results);
}
[Fact]

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

@ -22,8 +22,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 SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
@ -46,7 +46,7 @@ public class EventIntegrationHandlerTests
{
IntegrationType = IntegrationType.Webhook,
MessageId = "TestMessageId",
Configuration = new WebhookIntegrationConfigurationDetails(_url),
Configuration = new WebhookIntegrationConfigurationDetails(_uri),
RenderedTemplate = template,
RetryCount = 0,
DelayUntilDate = null
@ -62,7 +62,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];
@ -72,11 +72,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];
@ -212,7 +212,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)
{