diff --git a/src/Api/AdminConsole/Controllers/SlackOAuthController.cs b/src/Api/AdminConsole/Controllers/SlackOAuthController.cs new file mode 100644 index 0000000000..3e05abaa29 --- /dev/null +++ b/src/Api/AdminConsole/Controllers/SlackOAuthController.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using Bit.Core.Settings; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.AdminConsole.Controllers; + +[Route("slack/oauth")] +public class SlackOAuthController( + IHttpClientFactory httpClientFactory, + GlobalSettings globalSettings) + : Controller +{ + private readonly string _clientId = globalSettings.Slack.ClientId; + private readonly string _clientSecret = globalSettings.Slack.ClientSecret; + private readonly string _scopes = globalSettings.Slack.Scopes; + private readonly string _redirectUrl = globalSettings.Slack.RedirectUrl; + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + + public const string HttpClientName = "SlackOAuthContollerHttpClient"; + + [HttpGet("redirect")] + public IActionResult RedirectToSlack() + { + string slackOAuthUrl = $"https://slack.com/oauth/v2/authorize?client_id={_clientId}&scope={_scopes}&redirect_uri={_redirectUrl}"; + + return Redirect(slackOAuthUrl); + } + + [HttpGet("callback")] + public async Task OAuthCallback([FromQuery] string code) + { + if (string.IsNullOrEmpty(code)) + { + return BadRequest("Missing code from Slack."); + } + + var tokenResponse = await _httpClient.PostAsync("https://slack.com/api/oauth.v2.access", + new FormUrlEncodedContent(new[] + { + new KeyValuePair("client_id", _clientId), + new KeyValuePair("client_secret", _clientSecret), + new KeyValuePair("code", code), + new KeyValuePair("redirect_uri", _redirectUrl) + })); + + var responseBody = await tokenResponse.Content.ReadAsStringAsync(); + var jsonDoc = JsonDocument.Parse(responseBody); + var root = jsonDoc.RootElement; + + if (!root.GetProperty("ok").GetBoolean()) + { + return BadRequest($"OAuth failed: {root.GetProperty("error").GetString()}"); + } + + string botToken = root.GetProperty("access_token").GetString(); + string teamId = root.GetProperty("team").GetProperty("id").GetString(); + + SaveTokenToDatabase(teamId, botToken); + + return Ok("Slack OAuth successful. Your bot is now installed."); + } + + private void SaveTokenToDatabase(string teamId, string botToken) + { + Console.WriteLine($"Stored bot token for team {teamId}: {botToken}"); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs new file mode 100644 index 0000000000..347a12f405 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Bit.Core.Models.Data; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class SlackEventHandler( + GlobalSettings globalSettings, + SlackMessageSender slackMessageSender) + : IEventMessageHandler +{ + private readonly string _token = globalSettings.EventLogging.SlackToken; + private readonly string _email = globalSettings.EventLogging.SlackUserEmail; + + public async Task HandleEventAsync(EventMessage eventMessage) + { + await slackMessageSender.SendDirectMessageByEmailAsync( + _token, + JsonSerializer.Serialize(eventMessage), + _email + ); + } + + public async Task HandleManyEventsAsync(IEnumerable eventMessages) + { + await slackMessageSender.SendDirectMessageByEmailAsync( + _token, + JsonSerializer.Serialize(eventMessages), + _email + ); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackMessageSender.cs b/src/Core/AdminConsole/Services/Implementations/SlackMessageSender.cs new file mode 100644 index 0000000000..fa6c9da9b0 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/SlackMessageSender.cs @@ -0,0 +1,77 @@ +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class SlackMessageSender( + IHttpClientFactory httpClientFactory, + ILogger logger) +{ + private HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + + public const string HttpClientName = "SlackMessageSenderHttpClient"; + + public async Task SendDirectMessageByEmailAsync(string token, string message, string email) + { + var userId = await UserIdByEmail(token, email); + + if (userId is not null) + { + await SendSlackDirectMessageByUserId(token, message, userId); + } + } + + public async Task UserIdByEmail(string token, string email) + { + var request = new HttpRequestMessage(HttpMethod.Get, $"https://slack.com/api/users.lookupByEmail?email={email}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + var response = await _httpClient.SendAsync(request); + var content = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var root = content.RootElement; + + if (root.GetProperty("ok").GetBoolean()) + { + return root.GetProperty("user").GetProperty("id").GetString(); + } + else + { + logger.LogError("Error retrieving slack userId: " + root.GetProperty("error").GetString()); + return null; + } + } + + public async Task SendSlackDirectMessageByUserId(string token, string message, string userId) + { + var channelId = await OpenDmChannel(token, userId); + + var payload = JsonContent.Create(new { channel = channelId, text = message }); + var request = new HttpRequestMessage(HttpMethod.Post, "https://slack.com/api/chat.postMessage"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Content = payload; + + await _httpClient.SendAsync(request); + } + + public async Task OpenDmChannel(string token, string userId) + { + var payload = JsonContent.Create(new { users = userId }); + var request = new HttpRequestMessage(HttpMethod.Post, "https://slack.com/api/conversations.open"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Content = payload; + var response = await _httpClient.SendAsync(request); + var content = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); + var root = content.RootElement; + + if (root.GetProperty("ok").GetBoolean()) + { + return content.RootElement.GetProperty("channel").GetProperty("id").GetString(); + } + else + { + logger.LogError("Error opening DM channel: " + root.GetProperty("error").GetString()); + return null; + } + } +} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index dbfc8543a3..efadaf6a28 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings(); public virtual SqlSettings MySql { get; set; } = new SqlSettings(); public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" }; + public virtual SlackSettings Slack { get; set; } = new SlackSettings(); public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); @@ -269,9 +270,19 @@ public class GlobalSettings : IGlobalSettings } } + public class SlackSettings + { + public virtual string ClientId { get; set; } + public virtual string ClientSecret { get; set; } + public virtual string RedirectUrl { get; set; } + public virtual string Scopes { get; set; } + } + public class EventLoggingSettings { public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); + public virtual string SlackToken { get; set; } + public virtual string SlackUserEmail { get; set; } public virtual string WebhookUrl { get; set; } public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); @@ -305,6 +316,7 @@ public class GlobalSettings : IGlobalSettings public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; + public virtual string SlackQueueName { get; set; } = "events-slack-queue"; public string HostName { diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 57af285b03..f18ca8d506 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -117,6 +117,21 @@ public class Startup globalSettings, globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.SlackToken) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.SlackUserEmail)) + { + services.AddHttpClient(SlackMessageSender.HttpClientName); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.RabbitMq.SlackQueueName)); + } + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl)) { services.AddSingleton();