From 0f12d076ec1c3c07088b093dbca0f0e1dbe99004 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:30:51 -0700 Subject: [PATCH 1/6] fix(grantor-policies): [PM-21921] add null check (#5886) When getting grantor policies, adds a null check so that: - For a Grantor who is an org Owner, we respond with a `200` and the policies - For a Grantor is not an org Owner, we respond with a `200` and `null` --- src/Api/Auth/Controllers/EmergencyAccessController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 5d1f47de73..8b40444634 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -4,7 +4,6 @@ using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Response; using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Response; -using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Services; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -72,7 +71,7 @@ public class EmergencyAccessController : Controller { var user = await _userService.GetUserByPrincipalAsync(User); var policies = await _emergencyAccessService.GetPoliciesAsync(id, user); - var responses = policies.Select(policy => new PolicyResponseModel(policy)); + var responses = policies?.Select(policy => new PolicyResponseModel(policy)); return new ListResponseModel(responses); } From 5d9071da1f4bce178505b692d57ff6a4aaeae0c9 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:20:42 -0700 Subject: [PATCH 2/6] fix: failed db migrations due to missing kerberos tools (#6005) --- src/Admin/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Admin/Dockerfile b/src/Admin/Dockerfile index d6b42eadfb..0d6fd4cc78 100644 --- a/src/Admin/Dockerfile +++ b/src/Admin/Dockerfile @@ -59,6 +59,7 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends \ gosu \ curl \ + krb5-user \ && rm -rf /var/lib/apt/lists/* # Copy app from the build stage From 71aae24246f1a68b3481c188ee0a7242167001cb Mon Sep 17 00:00:00 2001 From: Andy Pixley <3723676+pixman20@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:39:35 -0400 Subject: [PATCH 3/6] [BRE-848] Fixing release workflow permissions (#6002) --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a9cc2d966..c62587fe39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,8 @@ jobs: name: Create GitHub release runs-on: ubuntu-22.04 needs: setup + permissions: + contents: write steps: - name: Download latest release Docker stubs if: ${{ inputs.release_type != 'Dry Run' }} From 49816e0926c7511705ef2080cdf4675f8d17f27f Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 26 Jun 2025 14:17:35 +0200 Subject: [PATCH 4/6] Remove unused feature flag "item-share" (#6003) Co-authored-by: Daniel James Smith --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 9438a23f9d..066d73f6d1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -194,7 +194,6 @@ public static class FeatureFlagKeys public const string IpcChannelFramework = "ipc-channel-framework"; /* Tools Team */ - public const string ItemShare = "item-share"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; /* Vault Team */ From 7fd1ccb7a2ff57ae07808ef44b893c00749a76de Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 26 Jun 2025 13:38:14 +0100 Subject: [PATCH 5/6] [PM-18699] Add trial path to Stripe metadata (#5940) * Add the market and product initiated change * Resolve the failing test * revert the change * resolve the failing test * Resolve the failing test * revert the add skipstrial * Revert add the new bool SkipTrial * Revert the changes * Revert the changes on the organizationsale * remove the trailsource property * Remove the CustomerSetup added * Add the improved code change for trial metadata * Revert the changes on GetSubscriptionSetup * Assign the InitiationPath --- src/Core/Billing/Models/Sales/OrganizationSale.cs | 1 + src/Core/Billing/Models/Sales/SubscriptionSetup.cs | 1 + .../Services/Implementations/OrganizationBillingService.cs | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 78ad26871b..c8ccb0f9a1 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -34,6 +34,7 @@ public class OrganizationSale var subscriptionSetup = GetSubscriptionSetup(signup); subscriptionSetup.SkipTrial = signup.SkipTrial; + subscriptionSetup.InitiationPath = signup.InitiationPath; return new OrganizationSale { diff --git a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs index 871a2920b1..5e891f75b6 100644 --- a/src/Core/Billing/Models/Sales/SubscriptionSetup.cs +++ b/src/Core/Billing/Models/Sales/SubscriptionSetup.cs @@ -10,6 +10,7 @@ public class SubscriptionSetup public required PasswordManager PasswordManagerOptions { get; set; } public SecretsManager? SecretsManagerOptions { get; set; } public bool SkipTrial = false; + public string? InitiationPath { get; set; } public class PasswordManager { diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index c67aa21971..725e274fa2 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -420,7 +420,11 @@ public class OrganizationBillingService( Items = subscriptionItemOptionsList, Metadata = new Dictionary { - ["organizationId"] = organizationId.ToString() + ["organizationId"] = organizationId.ToString(), + ["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) && + subscriptionSetup.InitiationPath.Contains("trial from marketing website") + ? "marketing-initiated" + : "product-initiated" }, OffSession = true, TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays From b418b07f26ce27434dd3e63e194f746d37da80db Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:19:49 -0400 Subject: [PATCH 6/6] [PM-17562] Add support for Auth on Webhook integration requests (#5970) * [PM-17562] Update documentation for event integrations * Fix SonarQube suggestion, bring ASB event listener in line with integration listener * Apply suggestions from code review Co-authored-by: Matt Bishop * Updates to README - PR fixes, additional context, tense alignment * Add links to different sections; remove inline code formatting in favor of single bacticks for JSON * [PM-17562] Add aupport for Auth on Webhook integration requests * Repsond to PR feedback - move optional params to end, add tests for optional cases --------- Co-authored-by: Matt Bishop --- .../EventIntegrations/SlackIntegration.cs | 2 +- .../SlackIntegrationConfiguration.cs | 2 +- .../SlackIntegrationConfigurationDetails.cs | 2 +- .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 2 +- .../SlackIntegrationHandler.cs | 4 +- .../WebhookIntegrationHandler.cs | 13 ++- ...ntegrationsConfigurationControllerTests.cs | 93 +++++++++++++++++-- ...tegrationConfigurationRequestModelTests.cs | 17 +++- .../IntegrationMessageTests.cs | 4 +- .../Services/EventIntegrationHandlerTests.cs | 6 +- .../Services/IntegrationHandlerTests.cs | 2 +- .../WebhookIntegrationHandlerTests.cs | 40 +++++++- 13 files changed, 160 insertions(+), 29 deletions(-) diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs index 1e6fbadb34..e8bfaee303 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegration(string token); +public record SlackIntegration(string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs index 13d1ad168f..2c757aeb76 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfiguration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegrationConfiguration(string channelId); +public record SlackIntegrationConfiguration(string ChannelId); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs index 219149e4f9..6c3d4c2fff 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/SlackIntegrationConfigurationDetails.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record SlackIntegrationConfigurationDetails(string channelId, string token); +public record SlackIntegrationConfigurationDetails(string ChannelId, string Token); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs index 5d00778e4b..ff28edc301 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfiguration.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record WebhookIntegrationConfiguration(string url); +public record WebhookIntegrationConfiguration(string Url, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs index 4790588c32..e0ed5dfcfa 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/WebhookIntegrationConfigurationDetails.cs @@ -2,4 +2,4 @@ namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; -public record WebhookIntegrationConfigurationDetails(string url); +public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs index f32d1166fa..6f55c0cf9c 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/SlackIntegrationHandler.cs @@ -11,9 +11,9 @@ public class SlackIntegrationHandler( public override async Task HandleAsync(IntegrationMessage message) { await slackService.SendSlackMessageByChannelIdAsync( - message.Configuration.token, + message.Configuration.Token, message.RenderedTemplate, - message.Configuration.channelId + message.Configuration.ChannelId ); return new IntegrationHandlerResult(success: true, message: message); diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs index 6dc348310d..b66df59a69 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/WebhookIntegrationHandler.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Net; +using System.Net.Http.Headers; using System.Text; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; @@ -20,8 +21,16 @@ public class WebhookIntegrationHandler( public override async Task HandleAsync(IntegrationMessage message) { - var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync(message.Configuration.url, content); + var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url); + request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); + if (!string.IsNullOrEmpty(message.Configuration.Scheme)) + { + request.Headers.Authorization = new AuthenticationHeaderValue( + scheme: message.Configuration.Scheme, + parameter: message.Configuration.Token + ); + } + var response = await _httpClient.SendAsync(request); var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); switch (response.StatusCode) diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index c72da2b16f..77283475cf 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -151,7 +151,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; @@ -188,7 +188,44 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + model.Configuration = JsonSerializer.Serialize(webhookConfig); + model.Template = "Template String"; + + var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration); + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(organizationIntegrationConfiguration); + var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Any()); + Assert.IsType(requestAction); + Assert.Equal(expected.Id, requestAction.Id); + Assert.Equal(expected.Configuration, requestAction.Configuration); + Assert.Equal(expected.EventType, requestAction.EventType); + Assert.Equal(expected.Template, requestAction.Template); + } + + [Theory, BitAutoData] + public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration, + OrganizationIntegrationConfiguration organizationIntegrationConfiguration, + OrganizationIntegrationConfigurationRequestModel model) + { + organizationIntegration.OrganizationId = organizationId; + organizationIntegration.Type = IntegrationType.Webhook; + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -350,7 +387,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = null; @@ -393,7 +430,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = "Template String"; @@ -436,7 +473,49 @@ 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(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); + model.Configuration = JsonSerializer.Serialize(webhookConfig); + model.Template = "Template String"; + + var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration)); + + sutProvider.Sut.Url = Substitute.For(); + sutProvider.GetDependency() + .OrganizationOwner(organizationId) + .Returns(true); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegration); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(organizationIntegrationConfiguration); + var requestAction = await sutProvider.Sut.UpdateAsync( + organizationId, + organizationIntegration.Id, + organizationIntegrationConfiguration.Id, + model); + + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Any()); + Assert.IsType(requestAction); + Assert.Equal(expected.Id, requestAction.Id); + Assert.Equal(expected.Configuration, requestAction.Configuration); + Assert.Equal(expected.EventType, requestAction.EventType); + Assert.Equal(expected.Template, requestAction.Template); + } + + [Theory, BitAutoData] + public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds( + SutProvider sutProvider, + Guid organizationId, + OrganizationIntegration organizationIntegration, + OrganizationIntegrationConfiguration organizationIntegrationConfiguration, + OrganizationIntegrationConfigurationRequestModel model) + { + organizationIntegration.OrganizationId = organizationId; + organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; + organizationIntegration.Type = IntegrationType.Webhook; + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -476,7 +555,7 @@ public class OrganizationIntegrationsConfigurationControllerTests { organizationIntegration.OrganizationId = organizationId; organizationIntegration.Type = IntegrationType.Webhook; - var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost"); + var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN"); model.Configuration = JsonSerializer.Serialize(webhookConfig); model.Template = "Template String"; @@ -582,7 +661,7 @@ public class OrganizationIntegrationsConfigurationControllerTests organizationIntegration.OrganizationId = organizationId; organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id; organizationIntegration.Type = IntegrationType.Slack; - var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456"); + var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456"); model.Configuration = JsonSerializer.Serialize(slackConfig); model.Template = null; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 19ae20804b..5958aa06aa 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -43,7 +43,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests [InlineData(" ")] public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template) { - var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); var model = new OrganizationIntegrationConfigurationRequestModel { Configuration = config, @@ -92,7 +92,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests } [Fact] - public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue() + public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue() { var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com")); var model = new OrganizationIntegrationConfigurationRequestModel @@ -104,6 +104,19 @@ public class OrganizationIntegrationConfigurationRequestModelTests Assert.True(model.IsValidForType(IntegrationType.Webhook)); } + [Fact] + public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue() + { + var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN")); + var model = new OrganizationIntegrationConfigurationRequestModel + { + Configuration = config, + Template = "template" + }; + + Assert.True(model.IsValidForType(IntegrationType.Webhook)); + } + [Fact] public void IsValidForType_UnknownIntegrationType_ReturnsFalse() { diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs index ebb6fa079e..a68bfd4fcb 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationMessageTests.cs @@ -14,7 +14,7 @@ public class IntegrationMessageTests { var message = new IntegrationMessage { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), MessageId = _messageId, RetryCount = 2, RenderedTemplate = string.Empty, @@ -34,7 +34,7 @@ public class IntegrationMessageTests { var message = new IntegrationMessage { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), MessageId = _messageId, RenderedTemplate = "This is the message", IntegrationType = IntegrationType.Webhook, diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index d4ceabae75..8dcc49a8f1 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -62,7 +62,7 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); config.Template = template; return [config]; @@ -72,11 +72,11 @@ public class EventIntegrationHandlerTests { var config = Substitute.For(); config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url }); config.Template = template; var config2 = Substitute.For(); config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 }); + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 }); config2.Template = template; return [config, config2]; diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs index 7f113cfe53..b4a384d798 100644 --- a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -14,7 +14,7 @@ public class IntegrationHandlerTests var sut = new TestIntegrationHandler(); var expected = new IntegrationMessage() { - Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"), MessageId = "TestMessageId", IntegrationType = IntegrationType.Webhook, RenderedTemplate = "Template", diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs index 676a975b77..9a03fb28f0 100644 --- a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http.Headers; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -16,6 +17,8 @@ public class WebhookIntegrationHandlerTests { private readonly MockedHttpMessageHandler _handler; private readonly HttpClient _httpClient; + private const string _scheme = "Bearer"; + private const string _token = "AUTH_TOKEN"; private const string _webhookUrl = "http://localhost/test/event"; public WebhookIntegrationHandlerTests() @@ -39,7 +42,7 @@ public class WebhookIntegrationHandlerTests } [Theory, BitAutoData] - public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage message) { var sutProvider = GetSutProvider(); message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); @@ -59,6 +62,33 @@ public class WebhookIntegrationHandlerTests var returned = await request.Content.ReadAsStringAsync(); Assert.Equal(HttpMethod.Post, request.Method); + Assert.Null(request.Headers.Authorization); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadAsStringAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization); Assert.Equal(_webhookUrl, request.RequestUri.ToString()); AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); } @@ -71,7 +101,7 @@ public class WebhookIntegrationHandlerTests var retryAfter = now.AddSeconds(60); sutProvider.GetDependency().SetUtcNow(now); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -94,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); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TooManyRequests) @@ -115,7 +145,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.InternalServerError) @@ -134,7 +164,7 @@ public class WebhookIntegrationHandlerTests public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) { var sutProvider = GetSutProvider(); - message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token); _handler.Fallback .WithStatusCode(HttpStatusCode.TemporaryRedirect)