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

Adjust URL structure; add delete for Slack, add tests

This commit is contained in:
Brant DeBow 2025-04-03 11:04:19 -04:00
parent df2ebff7a9
commit 09cff6e726
No known key found for this signature in database
GPG Key ID: 94411BB25947C72B
2 changed files with 107 additions and 29 deletions

View File

@ -11,21 +11,24 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[Route("slack/oauth")] [Route("organizations/{organizationId:guid}/integrations/slack/")]
[Authorize("Application")] [Authorize("Application")]
public class SlackOAuthController( public class SlackIntegrationController(
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository, IOrganizationIntegrationRepository integrationRepository,
ISlackService slackService) : Controller ISlackService slackService) : Controller
{ {
[HttpGet("redirect/{id:guid}")] [HttpGet("redirect/")]
public async Task<IActionResult> RedirectToSlack(Guid id) public async Task<IActionResult> RedirectAsync(Guid organizationId)
{ {
if (!await currentContext.OrganizationOwner(id)) if (!await currentContext.OrganizationOwner(organizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
string callbackUrl = Url.RouteUrl(nameof(OAuthCallback), new { id = id }, currentContext.HttpContext.Request.Scheme); string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { id = organizationId },
currentContext.HttpContext.Request.Scheme);
var redirectUrl = slackService.GetRedirectUrl(callbackUrl); var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
if (string.IsNullOrEmpty(redirectUrl)) if (string.IsNullOrEmpty(redirectUrl))
@ -36,10 +39,10 @@ public class SlackOAuthController(
return Redirect(redirectUrl); return Redirect(redirectUrl);
} }
[HttpGet("callback/{id:guid}", Name = nameof(OAuthCallback))] [HttpGet("create", Name = nameof(CreateAsync))]
public async Task<IActionResult> OAuthCallback(Guid id, [FromQuery] string code) public async Task<IActionResult> CreateAsync(Guid organizationId, [FromQuery] string code)
{ {
if (!await currentContext.OrganizationOwner(id)) if (!await currentContext.OrganizationOwner(organizationId))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
@ -49,7 +52,10 @@ public class SlackOAuthController(
throw new BadRequestException("Missing code from Slack."); throw new BadRequestException("Missing code from Slack.");
} }
string callbackUrl = Url.RouteUrl(nameof(OAuthCallback), new { id = id }, currentContext.HttpContext.Request.Scheme); string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { id = organizationId },
currentContext.HttpContext.Request.Scheme);
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl); var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
@ -59,10 +65,28 @@ public class SlackOAuthController(
var integration = await integrationRepository.CreateAsync(new OrganizationIntegration var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
{ {
OrganizationId = id, OrganizationId = organizationId,
Type = IntegrationType.Slack, Type = IntegrationType.Slack,
Configuration = JsonSerializer.Serialize(new SlackIntegration(token)), Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
}); });
return Ok(integration.Id); return Ok(new { id = integration.Id } );
}
[HttpDelete("{integrationId:guid}")]
[HttpPost("{integrationId:guid}/delete")]
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
{
if (!await currentContext.OrganizationOwner(organizationId))
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration is null)
{
throw new NotFoundException();
}
await integrationRepository.DeleteAsync(integration);
} }
} }

View File

@ -8,16 +8,17 @@ using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NSubstitute; using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit; using Xunit;
namespace Bit.Api.Test.AdminConsole.Controllers; namespace Bit.Api.Test.AdminConsole.Controllers;
[ControllerCustomize(typeof(SlackOAuthController))] [ControllerCustomize(typeof(SlackIntegrationController))]
[SutProviderCustomize] [SutProviderCustomize]
public class SlackOAuthControllerTests public class SlackIntegrationControllerTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OAuthCallback_AllParamsProvided_Succeeds(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task CreateAsync_AllParamsProvided_Succeeds(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
var token = "xoxb-test-token"; var token = "xoxb-test-token";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
@ -27,8 +28,10 @@ public class SlackOAuthControllerTests
sutProvider.GetDependency<ISlackService>() sutProvider.GetDependency<ISlackService>()
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>()) .ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.Returns(token); .Returns(token);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
var requestAction = await sutProvider.Sut.OAuthCallback(organizationId, "A_test_code"); .CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, "A_test_code");
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1) await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.CreateAsync(Arg.Any<OrganizationIntegration>()); .CreateAsync(Arg.Any<OrganizationIntegration>());
@ -36,18 +39,18 @@ public class SlackOAuthControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OAuthCallback_CodeIsEmpty_ThrowsBadRequest(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId) .OrganizationOwner(organizationId)
.Returns(true); .Returns(true);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.OAuthCallback(organizationId, string.Empty)); await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, string.Empty));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OAuthCallback_SlackServiceReturnsEmpty_ThrowsBadRequest(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
@ -57,11 +60,11 @@ public class SlackOAuthControllerTests
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>()) .ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.Returns(string.Empty); .Returns(string.Empty);
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.OAuthCallback(organizationId, "A_test_code")); await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task OAuthCallback_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
var token = "xoxb-test-token"; var token = "xoxb-test-token";
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
@ -72,11 +75,62 @@ public class SlackOAuthControllerTests
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>()) .ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
.Returns(token); .Returns(token);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.OAuthCallback(organizationId, "A_test_code")); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Redirect_Success(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task DeleteAsync_AllParamsProvided_Succeeds(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration)
{
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.DeleteAsync(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_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
}
[Theory, BitAutoData]
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
}
[Theory, BitAutoData]
public async Task RedirectAsync_Success(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
var expectedUrl = $"https://localhost/{organizationId}"; var expectedUrl = $"https://localhost/{organizationId}";
@ -89,14 +143,14 @@ public class SlackOAuthControllerTests
.HttpContext.Request.Scheme .HttpContext.Request.Scheme
.Returns("https"); .Returns("https");
var requestAction = await sutProvider.Sut.RedirectToSlack(organizationId); var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
var redirectResult = Assert.IsType<RedirectResult>(requestAction); var redirectResult = Assert.IsType<RedirectResult>(requestAction);
Assert.Equal(expectedUrl, redirectResult.Url); Assert.Equal(expectedUrl, redirectResult.Url);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Redirect_SlackServiceReturnsEmpty_ThrowsNotFound(SutProvider<SlackOAuthController> sutProvider, Guid organizationId) public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty); sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
@ -107,11 +161,11 @@ public class SlackOAuthControllerTests
.HttpContext.Request.Scheme .HttpContext.Request.Scheme
.Returns("https"); .Returns("https");
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectToSlack(organizationId)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task Redirect_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackOAuthController> sutProvider, public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider,
Guid organizationId) Guid organizationId)
{ {
sutProvider.Sut.Url = Substitute.For<IUrlHelper>(); sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
@ -123,6 +177,6 @@ public class SlackOAuthControllerTests
.HttpContext.Request.Scheme .HttpContext.Request.Scheme
.Returns("https"); .Returns("https");
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectToSlack(organizationId)); await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
} }
} }