diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs new file mode 100644 index 0000000000..139a7aff25 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetails.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Nodes; +using Bit.Core.Enums; + +#nullable enable + +namespace Bit.Core.Models.Data.Organizations; + +public class OrganizationIntegrationConfigurationDetails +{ + public Guid Id { get; set; } + public Guid OrganizationIntegrationId { get; set; } + public IntegrationType IntegrationType { get; set; } + public EventType EventType { get; set; } + public string? Configuration { get; set; } + public string? IntegrationConfiguration { get; set; } + public string? Template { get; set; } + + public JsonObject MergedConfiguration + { + get + { + var integrationJson = IntegrationConfigurationJson; + + foreach (var kvp in ConfigurationJson) + { + integrationJson[kvp.Key] = kvp.Value?.DeepClone(); + } + + return integrationJson; + } + } + + private JsonObject ConfigurationJson + { + get + { + try + { + var configuration = Configuration ?? string.Empty; + return JsonNode.Parse(configuration) as JsonObject ?? new JsonObject(); + } + catch + { + return new JsonObject(); + } + } + } + + private JsonObject IntegrationConfigurationJson + { + get + { + try + { + var integration = IntegrationConfiguration ?? string.Empty; + return JsonNode.Parse(integration) as JsonObject ?? new JsonObject(); + } + catch + { + return new JsonObject(); + } + } + } +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs new file mode 100644 index 0000000000..516918fff9 --- /dev/null +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationConfigurationRepository.cs @@ -0,0 +1,13 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Core.Repositories; + +public interface IOrganizationIntegrationConfigurationRepository : IRepository +{ + Task> GetConfigurationDetailsAsync( + Guid organizationId, + IntegrationType integrationType, + EventType eventType); +} diff --git a/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs new file mode 100644 index 0000000000..cd7700c310 --- /dev/null +++ b/src/Core/AdminConsole/Repositories/IOrganizationIntegrationRepository.cs @@ -0,0 +1,7 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.Repositories; + +public interface IOrganizationIntegrationRepository : IRepository +{ +} diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs new file mode 100644 index 0000000000..f3227dfd22 --- /dev/null +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -0,0 +1,43 @@ +using System.Data; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.AdminConsole.Repositories; + +public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository +{ + public OrganizationIntegrationConfigurationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public OrganizationIntegrationConfigurationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetConfigurationDetailsAsync( + Guid organizationId, + IntegrationType integrationType, + EventType eventType) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]", + new + { + EventType = eventType, + OrganizationId = organizationId, + IntegrationType = integrationType + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } +} diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs new file mode 100644 index 0000000000..99f0e35378 --- /dev/null +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -0,0 +1,16 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Repositories; +using Bit.Core.Settings; + +namespace Bit.Infrastructure.Dapper.Repositories; + +public class OrganizationIntegrationRepository : Repository, IOrganizationIntegrationRepository +{ + public OrganizationIntegrationRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public OrganizationIntegrationRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs new file mode 100644 index 0000000000..f051830035 --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationConfigurationRepository.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; + +public class OrganizationIntegrationConfigurationRepository : Repository, IOrganizationIntegrationConfigurationRepository +{ + public OrganizationIntegrationConfigurationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations) + { } + + public async Task> GetConfigurationDetailsAsync( + Guid organizationId, + IntegrationType integrationType, + EventType eventType) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery( + organizationId, eventType, integrationType + ); + return await query.Run(dbContext).ToListAsync(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs new file mode 100644 index 0000000000..816ad3b25f --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationIntegrationRepository.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; + +public class OrganizationIntegrationRepository : Repository, IOrganizationIntegrationRepository +{ + public OrganizationIntegrationRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationIntegrations) + { } +} diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs new file mode 100644 index 0000000000..1a54d6588a --- /dev/null +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery.cs @@ -0,0 +1,39 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery : IQuery +{ + private readonly Guid _organizationId; + private readonly EventType _eventType; + private readonly IntegrationType _integrationType; + + public OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(Guid organizationId, EventType eventType, IntegrationType integrationType) + { + _organizationId = organizationId; + _eventType = eventType; + _integrationType = integrationType; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from oic in dbContext.OrganizationIntegrationConfigurations + join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id into oioic + from oi in dbContext.OrganizationIntegrations + where oi.OrganizationId == _organizationId && + oi.Type == _integrationType && + oic.EventType == _eventType + select new OrganizationIntegrationConfigurationDetails() + { + Id = oic.Id, + OrganizationIntegrationId = oic.OrganizationIntegrationId, + IntegrationType = oi.Type, + EventType = oic.EventType, + Configuration = oic.Configuration, + IntegrationConfiguration = oi.Configuration, + Template = oic.Template + }; + return query; + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 3f805bbe2c..ad6c7cf369 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -78,6 +78,8 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index dd1b97b4f2..5c1c1bc87f 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -55,6 +55,8 @@ public class DatabaseContext : DbContext public DbSet OrganizationApiKeys { get; set; } public DbSet OrganizationSponsorships { get; set; } public DbSet OrganizationConnections { get; set; } + public DbSet OrganizationIntegrations { get; set; } + public DbSet OrganizationIntegrationConfigurations { get; set; } public DbSet OrganizationUsers { get; set; } public DbSet Policies { get; set; } public DbSet Providers { get; set; } diff --git a/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs b/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs new file mode 100644 index 0000000000..99a11903b4 --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/Organizations/OrganizationIntegrationConfigurationDetailsTests.cs @@ -0,0 +1,82 @@ +using System.Text.Json; +using Bit.Core.Models.Data.Organizations; +using Xunit; + +namespace Bit.Core.Test.Models.Data.Organizations; + +public class OrganizationIntegrationConfigurationDetailsTests +{ + [Fact] + public void MergedConfiguration_WithValidConfigAndIntegration_ReturnsMergedJson() + { + var config = new { config = "A new config value" }; + var integration = new { integration = "An integration value" }; + var expectedObj = new { integration = "An integration value", config = "A new config value" }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = JsonSerializer.Serialize(config); + sut.IntegrationConfiguration = JsonSerializer.Serialize(integration); + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } + + [Fact] + public void MergedConfiguration_WithInvalidJsonConfigAndIntegration_ReturnsEmptyJson() + { + var expectedObj = new { }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = "Not JSON"; + sut.IntegrationConfiguration = "Not JSON"; + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } + + [Fact] + public void MergedConfiguration_WithNullConfigAndIntegration_ReturnsEmptyJson() + { + var expectedObj = new { }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = null; + sut.IntegrationConfiguration = null; + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } + + [Fact] + public void MergedConfiguration_WithValidIntegrationAndNullConfig_ReturnsIntegrationJson() + { + var integration = new { integration = "An integration value" }; + var expectedObj = new { integration = "An integration value" }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = null; + sut.IntegrationConfiguration = JsonSerializer.Serialize(integration); + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } + + [Fact] + public void MergedConfiguration_WithValidConfigAndNullIntegration_ReturnsConfigJson() + { + var config = new { config = "A new config value" }; + var expectedObj = new { config = "A new config value" }; + var expected = JsonSerializer.Serialize(expectedObj); + + var sut = new OrganizationIntegrationConfigurationDetails(); + sut.Configuration = JsonSerializer.Serialize(config); + sut.IntegrationConfiguration = null; + + var result = sut.MergedConfiguration; + Assert.Equal(expected, result.ToJsonString()); + } +}