diff --git a/src/Api/AdminConsole/Controllers/SlackOAuthController.cs b/src/Api/AdminConsole/Controllers/SlackOAuthController.cs index 15a9fe3300..442f4c0d73 100644 --- a/src/Api/AdminConsole/Controllers/SlackOAuthController.cs +++ b/src/Api/AdminConsole/Controllers/SlackOAuthController.cs @@ -1,4 +1,6 @@ -using Bit.Core.Context; +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Integrations; @@ -13,14 +15,13 @@ namespace Bit.Api.AdminConsole.Controllers; [Authorize("Application")] public class SlackOAuthController( ICurrentContext currentContext, - IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository, + IOrganizationIntegrationRepository integrationRepository, ISlackService slackService) : Controller { - [HttpGet("redirect/{id}")] - public async Task RedirectToSlack(string id) + [HttpGet("redirect/{id:guid}")] + public async Task RedirectToSlack(Guid id) { - var orgIdGuid = new Guid(id); - if (!await currentContext.OrganizationOwner(orgIdGuid)) + if (!await currentContext.OrganizationOwner(id)) { throw new NotFoundException(); } @@ -35,11 +36,10 @@ public class SlackOAuthController( return Redirect(redirectUrl); } - [HttpGet("callback/{id}", Name = nameof(OAuthCallback))] - public async Task OAuthCallback(string id, [FromQuery] string code) + [HttpGet("callback/{id:guid}", Name = nameof(OAuthCallback))] + public async Task OAuthCallback(Guid id, [FromQuery] string code) { - var orgIdGuid = new Guid(id); - if (!await currentContext.OrganizationOwner(orgIdGuid)) + if (!await currentContext.OrganizationOwner(id)) { throw new NotFoundException(); } @@ -49,7 +49,7 @@ public class SlackOAuthController( throw new BadRequestException("Missing code from Slack."); } - string callbackUrl = Url.RouteUrl(nameof(OAuthCallback)); + string callbackUrl = Url.RouteUrl(nameof(OAuthCallback), new { id = id }, currentContext.HttpContext.Request.Scheme); var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl); if (string.IsNullOrEmpty(token)) @@ -57,10 +57,12 @@ public class SlackOAuthController( throw new BadRequestException("Invalid response from Slack."); } - await integrationConfigurationRepository.CreateOrganizationIntegrationAsync( - orgIdGuid, - IntegrationType.Slack, - new SlackIntegration(token)); - return Ok("Slack OAuth successful. Your bot is now installed."); + var integration = await integrationRepository.CreateAsync(new OrganizationIntegration + { + OrganizationId = id, + Type = IntegrationType.Slack, + Configuration = JsonSerializer.Serialize(new SlackIntegration(token)), + }); + return Ok("Your bot is now installed."); } } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index eb386c538a..e4c8ed7db7 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -30,7 +30,6 @@ using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; -using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; @@ -223,14 +222,10 @@ public class Startup { services.AddHttpClient(SlackService.HttpClientName); services.AddSingleton(); - services.AddSingleton(); } else { services.AddSingleton(); - services.AddSingleton(); } } diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackConfiguration.cs index f54f7496c6..3e1c12545f 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackConfiguration.cs @@ -1,17 +1,3 @@ namespace Bit.Core.Models.Data.Integrations; -public class SlackConfiguration -{ - public SlackConfiguration() - { - } - - public SlackConfiguration(string channelId, string token) - { - ChannelId = channelId; - Token = token; - } - - public string Token { get; set; } = string.Empty; - public string ChannelId { get; set; } = string.Empty; -} +public record SlackConfiguration(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookConfiguration.cs index 176ad75d11..436443029a 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookConfiguration.cs @@ -1,7 +1,3 @@ namespace Bit.Core.Models.Data.Integrations; -public class WebhookConfiguration -{ - public string Url { get; set; } = string.Empty; - public string ApiKey { get; set; } = string.Empty; -} +public record WebhookConfiguration(string url); diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs index 6d780ed00f..487d2ea12c 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -1,17 +1,12 @@ -using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; namespace Bit.Core.Repositories; -public interface IOrganizationIntegrationConfigurationRepository +public interface IOrganizationIntegrationConfigurationRepository : IRepository { - Task>> GetConfigurationsAsync( + Task> GetConfigurationsAsync( Guid organizationId, IntegrationType integrationType, EventType eventType); - - Task CreateOrganizationIntegrationAsync( - Guid organizationId, - IntegrationType integrationType, - T configuration); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs new file mode 100644 index 0000000000..cd7700c310 --- /dev/null +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs @@ -0,0 +1,7 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.Repositories; + +public interface IOrganizationIntegrationRepository : IRepository +{ +} diff --git a/src/Core/AdminConsole/Repositories/LocalOrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/LocalOrganizationIntegrationConfigurationRepository.cs deleted file mode 100644 index cbe96213a9..0000000000 --- a/src/Core/AdminConsole/Repositories/LocalOrganizationIntegrationConfigurationRepository.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; -using Bit.Core.Settings; - -namespace Bit.Core.Repositories; - -public class LocalOrganizationIntegrationConfigurationRepository(GlobalSettings globalSettings) - : IOrganizationIntegrationConfigurationRepository -{ - public async Task>> GetConfigurationsAsync(Guid organizationId, - IntegrationType integrationType, - EventType eventType) - { - var configurations = new List>(); - switch (integrationType) - { - case IntegrationType.Slack: - foreach (var configuration in globalSettings.EventLogging.SlackConfigurations) - { - configurations.Add(new IntegrationConfiguration - { - Configuration = configuration, - Template = "This is a test of the new Slack integration, #UserId#, #Type#, #Date#" - } as IntegrationConfiguration); - } - break; - case IntegrationType.Webhook: - foreach (var configuration in globalSettings.EventLogging.WebhookConfigurations) - { - configurations.Add(new IntegrationConfiguration - { - Configuration = configuration, - Template = "{ \"Date\": \"#Date#\", \"Type\": \"#Type#\", \"UserId\": \"#UserId#\" }" - } as IntegrationConfiguration); - } - break; - } - - return configurations; - } - - public async Task CreateOrganizationIntegrationAsync( - Guid organizationId, - IntegrationType integrationType, - T configuration) - { - var json = JsonSerializer.Serialize(configuration); - - Console.WriteLine($"Organization: {organizationId}, IntegrationType: {integrationType}, Configuration: {json}"); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs index c63a12e628..1b40fb69a4 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using System.Text.Json; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; @@ -12,15 +13,21 @@ public class SlackEventHandler( { public async Task HandleEventAsync(EventMessage eventMessage) { - var organizationId = eventMessage.OrganizationId ?? Guid.NewGuid(); - var configurations = await configurationRepository.GetConfigurationsAsync(organizationId, IntegrationType.Slack, eventMessage.Type); + var organizationId = eventMessage.OrganizationId ?? Guid.Empty; + var configurations = await configurationRepository.GetConfigurationsAsync(organizationId, IntegrationType.Slack, eventMessage.Type); foreach (var configuration in configurations) { + var config = JsonSerializer.Deserialize(configuration.Configuration ?? string.Empty); + if (config is null) + { + continue; + } + await slackService.SendSlackMessageByChannelIdAsync( - configuration.Configuration.Token, + config.token, TemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), - configuration.Configuration.ChannelId + config.channelId ); } } diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index 933d9c2eae..778850acc1 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Integrations; @@ -19,20 +20,25 @@ public class WebhookEventHandler( public async Task HandleEventAsync(EventMessage eventMessage) { - Guid organizationId = eventMessage.OrganizationId ?? Guid.NewGuid(); - - var configurations = await configurationRepository.GetConfigurationsAsync(organizationId, + var organizationId = eventMessage.OrganizationId ?? Guid.Empty; + var configurations = await configurationRepository.GetConfigurationsAsync(organizationId, IntegrationType.Webhook, eventMessage.Type); foreach (var configuration in configurations) { + var config = JsonSerializer.Deserialize(configuration.Configuration ?? string.Empty); + if (config is null) + { + continue; + } + var content = new StringContent( TemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), Encoding.UTF8, "application/json" ); var response = await _httpClient.PostAsync( - configuration.Configuration.Url, + config.url, content); response.EnsureSuccessStatusCode(); } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 5d9f2ee86e..33f7d1cb6b 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Context; using Bit.Core.IdentityServer; -using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -119,8 +118,6 @@ public class Startup globalSettings, globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); - services.AddSingleton(); - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs new file mode 100644 index 0000000000..3d5953d240 --- /dev/null +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -0,0 +1,42 @@ +using System.Data; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.AdminConsole.Repositories; + +public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository +{ + public OrganizationIntegrationConfigurationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public OrganizationIntegrationConfigurationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetConfigurationsAsync( + Guid organizationId, + IntegrationType integrationType, + EventType eventType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]", + new + { + EventType = eventType, + OrganizationId = organizationId, + IntegrationType = integrationType + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } +} diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs new file mode 100644 index 0000000000..99f0e35378 --- /dev/null +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -0,0 +1,16 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Repositories; +using Bit.Core.Settings; + +namespace Bit.Infrastructure.Dapper.Repositories; + +public class OrganizationIntegrationRepository : Repository, IOrganizationIntegrationRepository +{ + public OrganizationIntegrationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public OrganizationIntegrationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 26abf5632c..d48fe95096 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -41,6 +41,8 @@ public static class DapperServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton();