1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-07 05:58:13 -05:00

[PM-17562] Slack Event Investigation

This commit is contained in:
Brant DeBow 2025-02-24 15:30:41 -05:00
parent b66f255c5c
commit 17c0d66e37
No known key found for this signature in database
GPG Key ID: 94411BB25947C72B
5 changed files with 203 additions and 0 deletions

View File

@ -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<IActionResult> 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<string, string>("client_id", _clientId),
new KeyValuePair<string, string>("client_secret", _clientSecret),
new KeyValuePair<string, string>("code", code),
new KeyValuePair<string, string>("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}");
}
}

View File

@ -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<EventMessage> eventMessages)
{
await slackMessageSender.SendDirectMessageByEmailAsync(
_token,
JsonSerializer.Serialize(eventMessages),
_email
);
}
}

View File

@ -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<SlackMessageSender> 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<string> 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<string> 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;
}
}
}

View File

@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings
public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings(); public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();
public virtual SqlSettings MySql { 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 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 EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings();
public virtual MailSettings Mail { get; set; } = new MailSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings();
public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); 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 class EventLoggingSettings
{ {
public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings(); 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 virtual string WebhookUrl { get; set; }
public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings(); 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 EventRepositoryQueueName { get; set; } = "events-write-queue";
public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; public virtual string WebhookQueueName { get; set; } = "events-webhook-queue";
public virtual string SlackQueueName { get; set; } = "events-slack-queue";
public string HostName public string HostName
{ {

View File

@ -117,6 +117,21 @@ public class Startup
globalSettings, globalSettings,
globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.SlackToken) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.SlackUserEmail))
{
services.AddHttpClient(SlackMessageSender.HttpClientName);
services.AddSingleton<SlackMessageSender>();
services.AddSingleton<SlackEventHandler>();
services.AddSingleton<IHostedService>(provider =>
new RabbitMqEventListenerService(
provider.GetRequiredService<SlackEventHandler>(),
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
globalSettings,
globalSettings.EventLogging.RabbitMq.SlackQueueName));
}
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl)) if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl))
{ {
services.AddSingleton<WebhookEventHandler>(); services.AddSingleton<WebhookEventHandler>();