Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@
using Bit.Core.Enums;
using Bit.Core.Models.Api;

#nullable enable

namespace Bit.Api.AdminConsole.Models.Response.Organizations;

public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
{
public OrganizationIntegrationConfigurationResponseModel(OrganizationIntegrationConfiguration organizationIntegrationConfiguration, string obj = "organizationIntegrationConfiguration")
: base(obj)
{
ArgumentNullException.ThrowIfNull(organizationIntegrationConfiguration);

Id = organizationIntegrationConfiguration.Id;
Configuration = organizationIntegrationConfiguration.Configuration;
CreationDate = organizationIntegrationConfiguration.CreationDate;
Expand Down
6 changes: 6 additions & 0 deletions src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public class SlackOAuthResponse : SlackApiResponse
public SlackTeam Team { get; set; } = new();
}

public class SlackSendMessageResponse : SlackApiResponse
{
[JsonPropertyName("channel")]
public string Channel { get; set; } = string.Empty;
}

public class SlackTeam
{
public string Id { get; set; } = string.Empty;
Expand Down
8 changes: 5 additions & 3 deletions src/Core/AdminConsole/Services/ISlackService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
๏ปฟnamespace Bit.Core.Services;
๏ปฟusing Bit.Core.Models.Slack;

namespace Bit.Core.Services;

/// <summary>Defines operations for interacting with Slack, including OAuth authentication, channel discovery,
/// and sending messages.</summary>
Expand Down Expand Up @@ -54,6 +56,6 @@ public interface ISlackService
/// <param name="token">A valid Slack OAuth access token.</param>
/// <param name="message">The message text to send.</param>
/// <param name="channelId">The channel ID to send the message to.</param>
/// <returns>A task that completes when the message has been sent.</returns>
Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
/// <returns>The response from Slack after sending the message.</returns>
Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,43 @@ public class SlackIntegrationHandler(
ISlackService slackService)
: IntegrationHandlerBase<SlackIntegrationConfigurationDetails>
{
private static readonly HashSet<string> _retryableErrors = new(StringComparer.Ordinal)
{
"internal_error",
"message_limit_exceeded",
"rate_limited",
"ratelimited",
"service_unavailable"
};

public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
{
await slackService.SendSlackMessageByChannelIdAsync(
var slackResponse = await slackService.SendSlackMessageByChannelIdAsync(
message.Configuration.Token,
message.RenderedTemplate,
message.Configuration.ChannelId
);

return new IntegrationHandlerResult(success: true, message: message);
if (slackResponse is null)
{
return new IntegrationHandlerResult(success: false, message: message)
{
FailureReason = "Slack response was null"
};
}

if (slackResponse.Ok)
{
return new IntegrationHandlerResult(success: true, message: message);
}

var result = new IntegrationHandlerResult(success: false, message: message) { FailureReason = slackResponse.Error };

if (_retryableErrors.Contains(slackResponse.Error))
{
result.Retryable = true;
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
๏ปฟusing System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Web;
using Bit.Core.Models.Slack;
using Bit.Core.Settings;
Expand Down Expand Up @@ -71,7 +72,7 @@ public async Task<List<string>> GetChannelIdsAsync(string token, List<string> ch
public async Task<string> GetDmChannelByEmailAsync(string token, string email)
{
var userId = await GetUserIdByEmailAsync(token, email);
return await OpenDmChannel(token, userId);
return await OpenDmChannelAsync(token, userId);
}

public string GetRedirectUrl(string callbackUrl, string state)
Expand All @@ -97,21 +98,21 @@ public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
}

var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access",
new FormUrlEncodedContent(new[]
{
new FormUrlEncodedContent([
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
catch (JsonException ex)
{
logger.LogError(ex, "Error parsing SlackOAuthResponse: invalid JSON");
result = null;
}

Expand All @@ -129,22 +130,42 @@ public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
return result.AccessToken;
}

public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
public async Task<SlackSendMessageResponse?> 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);
var response = await _httpClient.SendAsync(request);

try
{
return await response.Content.ReadFromJsonAsync<SlackSendMessageResponse>();
}
catch (JsonException ex)
{
logger.LogError(ex, "Error parsing Slack message response: invalid JSON");
return null;
}
}

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>();
SlackUserResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
}
catch (JsonException ex)
{
logger.LogError(ex, "Error parsing SlackUserResponse: invalid JSON");
result = null;
}

if (result is null)
{
Expand All @@ -160,7 +181,7 @@ private async Task<string> GetUserIdByEmailAsync(string token, string email)
return result.User.Id;
}

private async Task<string> OpenDmChannel(string token, string userId)
private async Task<string> OpenDmChannelAsync(string token, string userId)
{
if (string.IsNullOrEmpty(userId))
return string.Empty;
Expand All @@ -170,7 +191,16 @@ private async Task<string> OpenDmChannel(string token, string userId)
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Content = payload;
var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
SlackDmResponse? result;
try
{
result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
}
catch (JsonException ex)
{
logger.LogError(ex, "Error parsing SlackDmResponse: invalid JSON");
result = null;
}

if (result is null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
๏ปฟusing Bit.Core.Services;
๏ปฟusing Bit.Core.Models.Slack;
using Bit.Core.Services;

namespace Bit.Core.AdminConsole.Services.NoopImplementations;

Expand All @@ -24,9 +25,10 @@ public string GetRedirectUrl(string callbackUrl, string state)
return string.Empty;
}

public Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
public Task<SlackSendMessageResponse?> SendSlackMessageByChannelIdAsync(string token, string message,
string channelId)
{
return Task.FromResult(0);
return Task.FromResult<SlackSendMessageResponse?>(null);
}

public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1
.DeleteAsync(organizationIntegration);
}

[Theory, BitAutoData]
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
organizationIntegration.OrganizationId = organizationId;
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(organizationIntegration);

await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id);

await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.DeleteAsync(organizationIntegration);
}

[Theory, BitAutoData]
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationController> sutProvider,
Expand Down
Loading
Loading