using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations;
using Bit.Core.Dirt.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.Dirt.Models.Data.EventIntegrations;
using Bit.Core.Dirt.Models.Data.Teams;
using Bit.Core.Dirt.Repositories;
using Bit.Core.Dirt.Services;
using Bit.Core.Dirt.Services.Implementations;
using Bit.Core.Dirt.Services.NoopImplementations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ZiggyCreatures.Caching.Fusion;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
namespace Microsoft.Extensions.DependencyInjection;
public static class EventIntegrationsServiceCollectionExtensions
{
///
/// Adds all event integrations commands, queries, and required cache infrastructure.
/// This method is idempotent and can be called multiple times safely.
///
public static IServiceCollection AddEventIntegrationsCommandsQueries(
this IServiceCollection services,
GlobalSettings globalSettings)
{
// Ensure cache is registered first - commands depend on this keyed cache.
// This is idempotent for the same named cache, so it's safe to call.
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
// Add Validator
services.TryAddSingleton();
// Add all commands/queries
services.AddOrganizationIntegrationCommandsQueries();
services.AddOrganizationIntegrationConfigurationCommandsQueries();
return services;
}
///
/// Registers event write services based on available configuration.
///
/// The service collection to add services to.
/// The global settings containing event logging configuration.
/// The service collection for chaining.
///
///
/// This method registers the appropriate IEventWriteService implementation based on the available
/// configuration, checking in the following priority order:
///
///
/// 1. Azure Service Bus - If all Azure Service Bus settings are present, registers
/// EventIntegrationEventWriteService with AzureServiceBusService as the publisher
///
///
/// 2. RabbitMQ - If all RabbitMQ settings are present, registers EventIntegrationEventWriteService with
/// RabbitMqService as the publisher
///
///
/// 3. Azure Queue Storage - If Events.ConnectionString is present, registers AzureQueueEventWriteService
///
///
/// 4. Repository (Self-Hosted) - If SelfHosted is true, registers RepositoryEventWriteService
///
///
/// 5. Noop - If none of the above are configured, registers NoopEventWriteService (no-op implementation)
///
///
public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)
{
if (IsAzureServiceBusEnabled(globalSettings))
{
services.TryAddSingleton();
services.TryAddSingleton();
return services;
}
if (IsRabbitMqEnabled(globalSettings))
{
services.TryAddSingleton();
services.TryAddSingleton();
return services;
}
if (CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.Events.QueueName))
{
services.TryAddSingleton();
return services;
}
if (globalSettings.SelfHosted)
{
services.TryAddSingleton();
return services;
}
services.TryAddSingleton();
return services;
}
///
/// Registers Azure Service Bus-based event integration listeners and supporting infrastructure.
///
/// The service collection to add services to.
/// The global settings containing Azure Service Bus configuration.
/// The service collection for chaining.
///
///
/// If Azure Service Bus is not enabled (missing required settings), this method returns immediately
/// without registering any services.
///
///
/// When Azure Service Bus is enabled, this method registers:
/// - IAzureServiceBusService and IEventIntegrationPublisher implementations
/// - Table Storage event repository
/// - Azure Table Storage event handler
/// - All event integration services via AddEventIntegrationServices
///
///
/// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,
/// as it is required to create the event integrations extended cache.
///
///
public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!IsAzureServiceBusEnabled(globalSettings))
{
return services;
}
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddKeyedSingleton("persistent");
services.TryAddSingleton();
services.AddEventIntegrationServices(globalSettings);
return services;
}
///
/// Registers RabbitMQ-based event integration listeners and supporting infrastructure.
///
/// The service collection to add services to.
/// The global settings containing RabbitMQ configuration.
/// The service collection for chaining.
///
///
/// If RabbitMQ is not enabled (missing required settings), this method returns immediately
/// without registering any services.
///
///
/// When RabbitMQ is enabled, this method registers:
/// - IRabbitMqService and IEventIntegrationPublisher implementations
/// - Event repository handler
/// - All event integration services via AddEventIntegrationServices
///
///
/// PREREQUISITE: Callers must ensure AddDistributedCache has been called before this method,
/// as it is required to create the event integrations extended cache.
///
///
public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!IsRabbitMqEnabled(globalSettings))
{
return services;
}
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
services.AddEventIntegrationServices(globalSettings);
return services;
}
///
/// Registers Slack integration services based on configuration settings.
///
/// The service collection to add services to.
/// The global settings containing Slack configuration.
/// The service collection for chaining.
///
/// If all required Slack settings are configured (ClientId, ClientSecret, Scopes), registers the full SlackService,
/// including an HttpClient for Slack API calls. Otherwise, registers a NoopSlackService that performs no operations.
///
public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
{
services.AddHttpClient(SlackService.HttpClientName);
services.TryAddSingleton();
}
else
{
services.TryAddSingleton();
}
return services;
}
///
/// Registers Microsoft Teams integration services based on configuration settings.
///
/// The service collection to add services to.
/// The global settings containing Teams configuration.
/// The service collection for chaining.
///
/// If all required Teams settings are configured (ClientId, ClientSecret, Scopes), registers:
/// - TeamsService and its interfaces (IBot, ITeamsService)
/// - IBotFrameworkHttpAdapter with Teams credentials
/// - HttpClient for Teams API calls
/// Otherwise, registers a NoopTeamsService that performs no operations.
///
public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes))
{
services.AddHttpClient(TeamsService.HttpClientName);
services.TryAddSingleton();
services.TryAddSingleton(sp => sp.GetRequiredService());
services.TryAddSingleton(sp => sp.GetRequiredService());
services.TryAddSingleton(_ =>
new BotFrameworkHttpAdapter(
new TeamsBotCredentialProvider(
clientId: globalSettings.Teams.ClientId,
clientSecret: globalSettings.Teams.ClientSecret
)
)
);
}
else
{
services.TryAddSingleton();
}
return services;
}
///
/// Registers event integration services including handlers, listeners, and supporting infrastructure.
///
/// The service collection to add services to.
/// The global settings containing integration configuration.
/// The service collection for chaining.
///
///
/// This method orchestrates the registration of all event integration components based on the enabled
/// message broker (Azure Service Bus or RabbitMQ). It is an internal method called by the public
/// entry points AddAzureServiceBusListeners and AddRabbitMqListeners.
///
///
/// NOTE: If both Azure Service Bus and RabbitMQ are configured, Azure Service Bus takes precedence. This means that
/// Azure Service Bus listeners will be registered (and RabbitMQ listeners will NOT) even if this event is called
/// from AddRabbitMqListeners when Azure Service Bus settings are configured.
///
///
/// PREREQUISITE: Callers must ensure AddDistributedCache has been called before invoking this method.
/// This method depends on distributed cache infrastructure being available for the keyed extended
/// cache registration.
///
///
/// Registered Services:
/// - Keyed ExtendedCache for event integrations
/// - Integration filter service
/// - Integration handlers for Slack, Webhook, Hec, Datadog, and Teams
/// - Hosted services for event and integration listeners (based on enabled message broker)
///
///
internal static IServiceCollection AddEventIntegrationServices(this IServiceCollection services,
GlobalSettings globalSettings)
{
// Add common services
// NOTE: AddDistributedCache must be called by the caller before this method
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
services.TryAddSingleton();
services.TryAddKeyedSingleton("persistent");
// Add services in support of handlers
services.AddSlackService(globalSettings);
services.AddTeamsService(globalSettings);
services.TryAddSingleton(TimeProvider.System);
services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);
// Add integration handlers
services.TryAddSingleton, SlackIntegrationHandler>();
services.TryAddSingleton, WebhookIntegrationHandler>();
services.TryAddSingleton, DatadogIntegrationHandler>();
services.TryAddSingleton, TeamsIntegrationHandler>();
var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);
var slackConfiguration = new SlackListenerConfiguration(globalSettings);
var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);
var hecConfiguration = new HecListenerConfiguration(globalSettings);
var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);
var teamsConfiguration = new TeamsListenerConfiguration(globalSettings);
if (IsAzureServiceBusEnabled(globalSettings))
{
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new AzureServiceBusEventListenerService(
configuration: repositoryConfiguration,
handler: provider.GetRequiredService(),
serviceBusService: provider.GetRequiredService(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = repositoryConfiguration.EventPrefetchCount,
MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService()
)
)
);
services.AddAzureServiceBusIntegration(slackConfiguration);
services.AddAzureServiceBusIntegration(webhookConfiguration);
services.AddAzureServiceBusIntegration(hecConfiguration);
services.AddAzureServiceBusIntegration(datadogConfiguration);
services.AddAzureServiceBusIntegration(teamsConfiguration);
return services;
}
if (IsRabbitMqEnabled(globalSettings))
{
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new RabbitMqEventListenerService(
handler: provider.GetRequiredService(),
configuration: repositoryConfiguration,
rabbitMqService: provider.GetRequiredService(),
loggerFactory: provider.GetRequiredService()
)
)
);
services.AddRabbitMqIntegration(slackConfiguration);
services.AddRabbitMqIntegration(webhookConfiguration);
services.AddRabbitMqIntegration(hecConfiguration);
services.AddRabbitMqIntegration(datadogConfiguration);
services.AddRabbitMqIntegration(teamsConfiguration);
}
return services;
}
///
/// Registers Azure Service Bus-based event integration listeners for a specific integration type.
///
/// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).
/// The listener configuration type implementing IIntegrationListenerConfiguration.
/// The service collection to add services to.
/// The listener configuration containing routing keys and message processing settings.
/// The service collection for chaining.
///
///
/// This method registers three key components:
/// 1. EventIntegrationHandler - Keyed singleton for processing integration events
/// 2. AzureServiceBusEventListenerService - Hosted service for listening to event messages from Azure Service Bus
/// for this integration type
/// 3. AzureServiceBusIntegrationListenerService - Hosted service for listening to integration messages from
/// Azure Service Bus for this integration type
///
///
/// The handler uses the listener configuration's routing key as its service key, allowing multiple
/// handlers to be registered for different integration types.
///
///
/// Service Bus processor options (PrefetchCount and MaxConcurrentCalls) are configured from the listener
/// configuration to optimize message throughput and concurrency.
///
///
internal static IServiceCollection AddAzureServiceBusIntegration(this IServiceCollection services,
TListenerConfig listenerConfiguration)
where TConfig : class
where TListenerConfig : IIntegrationListenerConfiguration
{
services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
new EventIntegrationHandler(
integrationType: listenerConfiguration.IntegrationType,
eventIntegrationPublisher: provider.GetRequiredService(),
integrationFilterService: provider.GetRequiredService(),
cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
configurationRepository: provider.GetRequiredService(),
groupRepository: provider.GetRequiredService(),
organizationRepository: provider.GetRequiredService(),
organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
);
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new AzureServiceBusEventListenerService(
configuration: listenerConfiguration,
handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
serviceBusService: provider.GetRequiredService(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = listenerConfiguration.EventPrefetchCount,
MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService()
)
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new AzureServiceBusIntegrationListenerService(
configuration: listenerConfiguration,
handler: provider.GetRequiredService>(),
serviceBusService: provider.GetRequiredService(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = listenerConfiguration.IntegrationPrefetchCount,
MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService()
)
)
);
return services;
}
///
/// Registers RabbitMQ-based event integration listeners for a specific integration type.
///
/// The integration configuration details type (e.g., SlackIntegrationConfigurationDetails).
/// The listener configuration type implementing IIntegrationListenerConfiguration.
/// The service collection to add services to.
/// The listener configuration containing routing keys and message processing settings.
/// The service collection for chaining.
///
///
/// This method registers three key components:
/// 1. EventIntegrationHandler - Keyed singleton for processing integration events
/// 2. RabbitMqEventListenerService - Hosted service for listening to event messages from RabbitMQ for
/// this integration type
/// 3. RabbitMqIntegrationListenerService - Hosted service for listening to integration messages from RabbitMQ for
/// this integration type
///
///
///
/// The handler uses the listener configuration's routing key as its service key, allowing multiple
/// handlers to be registered for different integration types.
///
///
internal static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services,
TListenerConfig listenerConfiguration)
where TConfig : class
where TListenerConfig : IIntegrationListenerConfiguration
{
services.TryAddKeyedSingleton(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
new EventIntegrationHandler(
integrationType: listenerConfiguration.IntegrationType,
eventIntegrationPublisher: provider.GetRequiredService(),
integrationFilterService: provider.GetRequiredService(),
cache: provider.GetRequiredKeyedService(EventIntegrationsCacheConstants.CacheName),
configurationRepository: provider.GetRequiredService(),
groupRepository: provider.GetRequiredService(),
organizationRepository: provider.GetRequiredService(),
organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>())
);
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new RabbitMqEventListenerService(
handler: provider.GetRequiredKeyedService(serviceKey: listenerConfiguration.RoutingKey),
configuration: listenerConfiguration,
rabbitMqService: provider.GetRequiredService(),
loggerFactory: provider.GetRequiredService()
)
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton>(provider =>
new RabbitMqIntegrationListenerService(
handler: provider.GetRequiredService>(),
configuration: listenerConfiguration,
rabbitMqService: provider.GetRequiredService(),
loggerFactory: provider.GetRequiredService(),
timeProvider: provider.GetRequiredService()
)
)
);
return services;
}
internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services)
{
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
return services;
}
internal static IServiceCollection AddOrganizationIntegrationConfigurationCommandsQueries(this IServiceCollection services)
{
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
return services;
}
///
/// Determines if RabbitMQ is enabled for event integrations based on configuration settings.
///
/// The global settings containing RabbitMQ configuration.
/// True if all required RabbitMQ settings are present; otherwise, false.
///
/// Requires all the following settings to be configured:
///
/// - EventLogging.RabbitMq.HostName
/// - EventLogging.RabbitMq.Username
/// - EventLogging.RabbitMq.Password
/// - EventLogging.RabbitMq.EventExchangeName
/// - EventLogging.RabbitMq.IntegrationExchangeName
///
///
internal static bool IsRabbitMqEnabled(GlobalSettings settings)
{
return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.IntegrationExchangeName);
}
///
/// Determines if Azure Service Bus is enabled for event integrations based on configuration settings.
///
/// The global settings containing Azure Service Bus configuration.
/// True if all required Azure Service Bus settings are present; otherwise, false.
///
/// Requires all of the following settings to be configured:
///
/// - EventLogging.AzureServiceBus.ConnectionString
/// - EventLogging.AzureServiceBus.EventTopicName
/// - EventLogging.AzureServiceBus.IntegrationTopicName
///
///
internal static bool IsAzureServiceBusEnabled(GlobalSettings settings)
{
return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName) &&
CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.IntegrationTopicName);
}
}