1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-17562] Add GET endpoints for event integrations (#6104)

* [PM-17562] Add GET endpoints for event integrations

* Default to null for Service

* Respond to PR Feedback
This commit is contained in:
Brant DeBow
2025-07-23 14:24:59 -04:00
committed by GitHub
parent 829c3ed1d7
commit 988b994624
17 changed files with 402 additions and 4 deletions

View File

@@ -18,6 +18,27 @@ public class OrganizationIntegrationConfigurationController(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller
{
[HttpGet("")]
public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync(
Guid organizationId,
Guid integrationId)
{
if (!await HasPermission(organizationId))
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configurations = await integrationConfigurationRepository.GetManyByIntegrationAsync(integrationId);
return configurations
.Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration))
.ToList();
}
[HttpPost("")]
public async Task<OrganizationIntegrationConfigurationResponseModel> CreateAsync(
Guid organizationId,

View File

@@ -19,6 +19,20 @@ public class OrganizationIntegrationController(
ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository) : Controller
{
[HttpGet("")]
public async Task<List<OrganizationIntegrationResponseModel>> GetAsync(Guid organizationId)
{
if (!await HasPermission(organizationId))
{
throw new NotFoundException();
}
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
return integrations
.Select(integration => new OrganizationIntegrationResponseModel(integration))
.ToList();
}
[HttpPost("")]
public async Task<OrganizationIntegrationResponseModel> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)
{

View File

@@ -15,8 +15,10 @@ public class OrganizationIntegrationResponseModel : ResponseModel
Id = organizationIntegration.Id;
Type = organizationIntegration.Type;
Configuration = organizationIntegration.Configuration;
}
public Guid Id { get; set; }
public IntegrationType Type { get; set; }
public string? Configuration { get; set; }
}

View File

@@ -2,4 +2,4 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record HecIntegration(Uri Uri, string Scheme, string Token);
public record HecIntegration(Uri Uri, string Scheme, string Token, string? Service = null);

View File

@@ -12,4 +12,6 @@ public interface IOrganizationIntegrationConfigurationRepository : IRepository<O
EventType eventType);
Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();
Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationIntegrationId);
}

View File

@@ -4,4 +4,5 @@ namespace Bit.Core.Repositories;
public interface IOrganizationIntegrationRepository : IRepository<OrganizationIntegration, Guid>
{
Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId);
}

View File

@@ -52,4 +52,20 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Organiz
return results.ToList();
}
}
public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationIntegrationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationIntegrationConfiguration>(
"[dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]",
new
{
OrganizationIntegrationId = organizationIntegrationId
},
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
}

View File

@@ -1,6 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using System.Data;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Dapper;
using Microsoft.Data.SqlClient;
namespace Bit.Infrastructure.Dapper.Repositories;
@@ -13,4 +16,17 @@ public class OrganizationIntegrationRepository : Repository<OrganizationIntegrat
public OrganizationIntegrationRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{ }
public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationIntegration>(
"[dbo].[OrganizationIntegration_ReadManyByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
}

View File

@@ -3,6 +3,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
using Microsoft.EntityFrameworkCore;
@@ -40,4 +41,17 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Core.Ad
return await query.Run(dbContext).ToListAsync();
}
}
public async Task<List<Core.AdminConsole.Entities.OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(
Guid organizationIntegrationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var query = new OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery(
organizationIntegrationId
);
return await query.Run(dbContext).ToListAsync();
}
}
}

View File

@@ -1,14 +1,29 @@
using AutoMapper;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
public class OrganizationIntegrationRepository : Repository<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration, Guid>, IOrganizationIntegrationRepository
public class OrganizationIntegrationRepository :
Repository<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration, Guid>,
IOrganizationIntegrationRepository
{
public OrganizationIntegrationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationIntegrations)
{ }
{
}
public async Task<List<Core.AdminConsole.Entities.OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
var dbContext = GetDatabaseContext(scope);
var query = new OrganizationIntegrationReadManyByOrganizationIdQuery(organizationId);
return await query.Run(dbContext).ToListAsync();
}
}
}

View File

@@ -0,0 +1,33 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
public class OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery : IQuery<OrganizationIntegrationConfiguration>
{
private readonly Guid _organizationIntegrationId;
public OrganizationIntegrationConfigurationReadManyByOrganizationIntegrationIdQuery(Guid organizationIntegrationId)
{
_organizationIntegrationId = organizationIntegrationId;
}
public IQueryable<OrganizationIntegrationConfiguration> Run(DatabaseContext dbContext)
{
var query = from oic in dbContext.OrganizationIntegrationConfigurations
where oic.OrganizationIntegrationId == _organizationIntegrationId
select new OrganizationIntegrationConfiguration()
{
Id = oic.Id,
OrganizationIntegrationId = oic.OrganizationIntegrationId,
Configuration = oic.Configuration,
EventType = oic.EventType,
Filters = oic.Filters,
Template = oic.Template,
RevisionDate = oic.RevisionDate
};
return query;
}
}

View File

@@ -0,0 +1,30 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories.Queries;
public class OrganizationIntegrationReadManyByOrganizationIdQuery : IQuery<OrganizationIntegration>
{
private readonly Guid _organizationId;
public OrganizationIntegrationReadManyByOrganizationIdQuery(Guid organizationId)
{
_organizationId = organizationId;
}
public IQueryable<OrganizationIntegration> Run(DatabaseContext dbContext)
{
var query = from oi in dbContext.OrganizationIntegrations
where oi.OrganizationId == _organizationId
select new OrganizationIntegration()
{
Id = oi.Id,
OrganizationId = oi.OrganizationId,
Type = oi.Type,
Configuration = oi.Configuration,
};
return query;
}
}

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]
@OrganizationIntegrationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationIntegrationConfigurationView]
WHERE
[OrganizationIntegrationId] = @OrganizationIntegrationId
END

View File

@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationIntegration_ReadManyByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationIntegrationView]
WHERE
[OrganizationId] = @OrganizationId
END

View File

@@ -25,6 +25,60 @@ public class OrganizationIntegrationControllerTests
Type = IntegrationType.Webhook
};
[Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAsync(organizationId));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationsExist_ReturnsIntegrations(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId,
List<OrganizationIntegration> integrations)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns(integrations);
var result = await sutProvider.Sut.GetAsync(organizationId);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetManyByOrganizationAsync(organizationId);
Assert.Equal(integrations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationResponseModel>(r));
}
[Theory, BitAutoData]
public async Task GetAsync_NoIntegrations_ReturnsEmptyList(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
.GetManyByOrganizationAsync(organizationId)
.Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId);
Assert.Empty(result);
}
[Theory, BitAutoData]
public async Task CreateAsync_Webhook_AllParamsProvided_Succeeds(
SutProvider<OrganizationIntegrationController> sutProvider,

View File

@@ -141,6 +141,131 @@ public class OrganizationIntegrationsConfigurationControllerTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
}
[Theory, BitAutoData]
public async Task GetAsync_ConfigurationsExist_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId,
OrganizationIntegration organizationIntegration,
List<OrganizationIntegrationConfiguration> organizationIntegrationConfigurations)
{
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);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns(organizationIntegrationConfigurations);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result);
Assert.Equal(organizationIntegrationConfigurations.Count, result.Count);
Assert.All(result, r => Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(r));
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
[Theory, BitAutoData]
public async Task GetAsync_NoConfigurationsExist_ReturnsEmptyList(
SutProvider<OrganizationIntegrationConfigurationController> 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);
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
.GetManyByIntegrationAsync(Arg.Any<Guid>())
.Returns([]);
var result = await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id);
Assert.NotNull(result);
Assert.Empty(result);
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
.GetByIdAsync(organizationIntegration.Id);
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
.GetManyByIntegrationAsync(organizationIntegration.Id);
}
// [Theory, BitAutoData]
// public async Task GetAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
// SutProvider<OrganizationIntegrationConfigurationController> 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);
// sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
// .GetByIdAsync(Arg.Any<Guid>())
// .ReturnsNull();
//
// await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.Empty, Guid.Empty));
// }
//
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotExist_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> 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.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task GetAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> 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 Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, organizationIntegration.Id));
}
[Theory, BitAutoData]
public async Task GetAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
Guid organizationId)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(false);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetAsync(organizationId, Guid.NewGuid()));
}
[Theory, BitAutoData]
public async Task PostAsync_AllParamsProvided_Slack_Succeeds(
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,

View File

@@ -0,0 +1,29 @@
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegration_ReadManyByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationIntegrationView]
WHERE
[OrganizationId] = @OrganizationId
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByOrganizationIntegrationId]
@OrganizationIntegrationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationIntegrationConfigurationView]
WHERE
[OrganizationIntegrationId] = @OrganizationIntegrationId
END
GO