1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-10 04:14:41 -05:00
Files
bitwarden/src/Core/AdminConsole/Services/Implementations/SlackService.cs
Brant DeBow 90d831d9ef [PM-17562] API For Organization Integrations/Configurations, Refactored Distributed Events, Slack Integration (#5654)
* [PM-17562] Slack Event Investigation

* Refactored Slack and Webhook integrations to pull configurations dynamically from a new Repository

* Added new TemplateProcessor and added/updated unit tests

* SlackService improvements, testing, integration configurations

* Refactor SlackService to use a dedicated model to parse responses

* Refactored SlackOAuthController to use SlackService as an injected dependency; added tests for SlackService

* Remove unnecessary methods from the IOrganizationIntegrationConfigurationRepository

* Moved Slack OAuth to take into account the Organization it's being stored for. Added methods to store the top level integration for Slack

* Organization integrations and configuration database schemas

* Format EF files

* Initial buildout of basic repositories

* [PM-17562] Add Dapper Repositories For Organization Integrations and Configurations

* Update Slack and Webhook handlers to use new Repositories

* Update SlackOAuth tests to new signatures

* Added EF Repositories

* Update handlers to use latest repositories

* [PM-17562] Add Dapper and EF Repositories For Ogranization Integrations and Configurations

* Updated with changes from PR comments

* Adjusted Handlers to new repository method names; updated tests to naming convention

* Adjust URL structure; add delete for Slack, add tests

* Added Webhook Integration Controller

* Add tests for WebhookIntegrationController

* Added Create/Delete for  OrganizationIntegrationConfigurations

* Prepend ConnectionTypes into IntegrationType so we don't run into issues later

* Added Update to OrganizationIntegrationConfigurtionController

* Moved Webhook-specific integration code to being a generic controller for everything but Slack

* Removed delete from SlackController - Deletes should happen through the normal Integration controller

* Fixed SlackController, reworked OIC Controller to use ids from URL and update the returned object

* Added parse/type checking for integration and integration configuration JSONs, Cleaned up GlobalSettings to remove old values

* Cleanup and fixes for Azure Service Bus support

* Clean up naming on TemplateProcessorTests

* Address SonarQube warnings/suggestions

* Expanded test coverage; Cleaned up tests

* Respond to PR Feedback

* Rename TemplateProcessor to IntegrationTemplateProcessor

---------

Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
2025-04-23 10:44:43 -04:00

163 lines
6.0 KiB
C#

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Web;
using Bit.Core.Models.Slack;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class SlackService(
IHttpClientFactory httpClientFactory,
GlobalSettings globalSettings,
ILogger<SlackService> logger) : ISlackService
{
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
private readonly string _clientId = globalSettings.Slack.ClientId;
private readonly string _clientSecret = globalSettings.Slack.ClientSecret;
private readonly string _scopes = globalSettings.Slack.Scopes;
private readonly string _slackApiBaseUrl = globalSettings.Slack.ApiBaseUrl;
public const string HttpClientName = "SlackServiceHttpClient";
public async Task<string> GetChannelIdAsync(string token, string channelName)
{
return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault();
}
public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
{
var matchingChannelIds = new List<string>();
var baseUrl = $"{_slackApiBaseUrl}/conversations.list";
var nextCursor = string.Empty;
do
{
var uriBuilder = new UriBuilder(baseUrl);
var queryParameters = HttpUtility.ParseQueryString(uriBuilder.Query);
queryParameters["types"] = "public_channel,private_channel";
queryParameters["limit"] = "1000";
if (!string.IsNullOrEmpty(nextCursor))
{
queryParameters["cursor"] = nextCursor;
}
uriBuilder.Query = queryParameters.ToString();
var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackChannelListResponse>();
if (result is { Ok: true })
{
matchingChannelIds.AddRange(result.Channels
.Where(channel => channelNames.Contains(channel.Name))
.Select(channel => channel.Id));
nextCursor = result.ResponseMetadata.NextCursor;
}
else
{
logger.LogError("Error getting Channel Ids: {Error}", result.Error);
nextCursor = string.Empty;
}
} while (!string.IsNullOrEmpty(nextCursor));
return matchingChannelIds;
}
public async Task<string> GetDmChannelByEmailAsync(string token, string email)
{
var userId = await GetUserIdByEmailAsync(token, email);
return await OpenDmChannel(token, userId);
}
public string GetRedirectUrl(string redirectUrl)
{
return $"https://slack.com/oauth/v2/authorize?client_id={_clientId}&scope={_scopes}&redirect_uri={redirectUrl}";
}
public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
{
var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/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)
}));
SlackOAuthResponse result;
try
{
result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();
}
catch
{
result = null;
}
if (result == null)
{
logger.LogError("Error obtaining token via OAuth: Unknown error");
return string.Empty;
}
if (!result.Ok)
{
logger.LogError("Error obtaining token via OAuth: {Error}", result.Error);
return string.Empty;
}
return result.AccessToken;
}
public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
{
var payload = JsonContent.Create(new { channel = channelId, text = message });
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/chat.postMessage");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = payload;
await _httpClient.SendAsync(request);
}
private async Task<string> GetUserIdByEmailAsync(string token, string email)
{
var request = new HttpRequestMessage(HttpMethod.Get, $"{_slackApiBaseUrl}/users.lookupByEmail?email={email}");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
if (!result.Ok)
{
logger.LogError("Error retrieving Slack user ID: {Error}", result.Error);
return string.Empty;
}
return result.User.Id;
}
private async Task<string> OpenDmChannel(string token, string userId)
{
if (string.IsNullOrEmpty(userId))
return string.Empty;
var payload = JsonContent.Create(new { users = userId });
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/conversations.open");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = payload;
var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
if (!result.Ok)
{
logger.LogError("Error opening DM channel: {Error}", result.Error);
return string.Empty;
}
return result.Channel.Id;
}
}