Skip to content
Merged
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
@@ -1,8 +1,8 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -12,8 +12,10 @@ namespace Bit.Api.AdminConsole.Controllers;
[Authorize("Application")]
public class OrganizationIntegrationConfigurationController(
ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller
ICreateOrganizationIntegrationConfigurationCommand createCommand,
IUpdateOrganizationIntegrationConfigurationCommand updateCommand,
IDeleteOrganizationIntegrationConfigurationCommand deleteCommand,
IGetOrganizationIntegrationConfigurationsQuery getQuery) : Controller
{
[HttpGet("")]
public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAsync(
Expand All @@ -24,13 +26,8 @@ public async Task<List<OrganizationIntegrationConfigurationResponseModel>> GetAs
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}

var configurations = await integrationConfigurationRepository.GetManyByIntegrationAsync(integrationId);
var configurations = await getQuery.GetManyByIntegrationAsync(organizationId, integrationId);
return configurations
.Select(configuration => new OrganizationIntegrationConfigurationResponseModel(configuration))
.ToList();
Expand All @@ -46,19 +43,11 @@ public async Task<OrganizationIntegrationConfigurationResponseModel> CreateAsync
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!model.IsValidForType(integration.Type))
{
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
}

var organizationIntegrationConfiguration = model.ToOrganizationIntegrationConfiguration(integrationId);
var configuration = await integrationConfigurationRepository.CreateAsync(organizationIntegrationConfiguration);
return new OrganizationIntegrationConfigurationResponseModel(configuration);
var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
var created = await createCommand.CreateAsync(organizationId, integrationId, configuration);

return new OrganizationIntegrationConfigurationResponseModel(created);
}

[HttpPut("{configurationId:guid}")]
Expand All @@ -72,26 +61,11 @@ public async Task<OrganizationIntegrationConfigurationResponseModel> UpdateAsync
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!model.IsValidForType(integration.Type))
{
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
}

var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}

var newConfiguration = model.ToOrganizationIntegrationConfiguration(configuration);
await integrationConfigurationRepository.ReplaceAsync(newConfiguration);
var configuration = model.ToOrganizationIntegrationConfiguration(integrationId);
var updated = await updateCommand.UpdateAsync(organizationId, integrationId, configurationId, configuration);

return new OrganizationIntegrationConfigurationResponseModel(newConfiguration);
return new OrganizationIntegrationConfigurationResponseModel(updated);
}

[HttpDelete("{configurationId:guid}")]
Expand All @@ -101,19 +75,8 @@ public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid conf
{
throw new NotFoundException();
}
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}

var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}

await integrationConfigurationRepository.DeleteAsync(configuration);
await deleteCommand.DeleteAsync(organizationId, integrationId, configurationId);
}

[HttpPost("{configurationId:guid}/delete")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;


Expand All @@ -16,38 +14,6 @@ public class OrganizationIntegrationConfigurationRequestModel

public string? Template { get; set; }

public bool IsValidForType(IntegrationType integrationType)
{
switch (integrationType)
{
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
return false;
case IntegrationType.Slack:
return !string.IsNullOrWhiteSpace(Template) &&
IsConfigurationValid<SlackIntegrationConfiguration>() &&
IsFiltersValid();
case IntegrationType.Webhook:
return !string.IsNullOrWhiteSpace(Template) &&
IsConfigurationValid<WebhookIntegrationConfiguration>() &&
IsFiltersValid();
case IntegrationType.Hec:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
case IntegrationType.Datadog:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
case IntegrationType.Teams:
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
default:
return false;

}
}

public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)
{
return new OrganizationIntegrationConfiguration()
Expand All @@ -59,50 +25,4 @@ public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfigurati
Template = Template
};
}

public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(OrganizationIntegrationConfiguration currentConfiguration)
{
currentConfiguration.Configuration = Configuration;
currentConfiguration.EventType = EventType;
currentConfiguration.Filters = Filters;
currentConfiguration.Template = Template;

return currentConfiguration;
}

private bool IsConfigurationValid<T>()
{
if (string.IsNullOrWhiteSpace(Configuration))
{
return false;
}

try
{
var config = JsonSerializer.Deserialize<T>(Configuration);
return config is not null;
}
catch
{
return false;
}
}

private bool IsFiltersValid()
{
if (Filters is null)
{
return true;
}

try
{
var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(Filters);
return filters is not null;
}
catch
{
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand All @@ -20,8 +23,12 @@ public static IServiceCollection AddEventIntegrationsCommandsQueries(
// This is idempotent for the same named cache, so it's safe to call.
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);

// Add Validator
services.TryAddSingleton<IOrganizationIntegrationConfigurationValidator, OrganizationIntegrationConfigurationValidator>();

// Add all commands/queries
services.AddOrganizationIntegrationCommandsQueries();
services.AddOrganizationIntegrationConfigurationCommandsQueries();

return services;
}
Expand All @@ -35,4 +42,14 @@ internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(thi

return services;
}

internal static IServiceCollection AddOrganizationIntegrationConfigurationCommandsQueries(this IServiceCollection services)
{
services.TryAddScoped<ICreateOrganizationIntegrationConfigurationCommand, CreateOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IUpdateOrganizationIntegrationConfigurationCommand, UpdateOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IDeleteOrganizationIntegrationConfigurationCommand, DeleteOrganizationIntegrationConfigurationCommand>();
services.TryAddScoped<IGetOrganizationIntegrationConfigurationsQuery, GetOrganizationIntegrationConfigurationsQuery>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ZiggyCreatures.Caching.Fusion;

namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;

/// <summary>
/// Command implementation for creating organization integration configurations with validation and cache invalidation support.
/// </summary>
public class CreateOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,
IOrganizationIntegrationConfigurationValidator validator)
: ICreateOrganizationIntegrationConfigurationCommand
{
public async Task<OrganizationIntegrationConfiguration> CreateAsync(
Guid organizationId,
Guid integrationId,
OrganizationIntegrationConfiguration configuration)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
if (!validator.ValidateConfiguration(integration.Type, configuration))
{
throw new BadRequestException(
$"Invalid Configuration and/or Filters for integration type {integration.Type}");
}

var created = await configurationRepository.CreateAsync(configuration);

// Invalidate the cached configuration details
// Even though this is a new record, the cache could hold a stale empty list for this
if (created.EventType == null)
{
// Wildcard configuration - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
}
else
{
// Specific event type - only invalidate that specific cache entry
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: created.EventType.Value
));
}

return created;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.Extensions.DependencyInjection;
using ZiggyCreatures.Caching.Fusion;

namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;

/// <summary>
/// Command implementation for deleting organization integration configurations with cache invalidation support.
/// </summary>
public class DeleteOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)
: IDeleteOrganizationIntegrationConfigurationCommand
{
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}
var configuration = await configurationRepository.GetByIdAsync(configurationId);
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
{
throw new NotFoundException();
}

await configurationRepository.DeleteAsync(configuration);

if (configuration.EventType == null)
{
// Wildcard configuration - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
}
else
{
// Specific event type - only invalidate that specific cache entry
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: configuration.EventType.Value
));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;

namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations;

/// <summary>
/// Query implementation for retrieving organization integration configurations.
/// </summary>
public class GetOrganizationIntegrationConfigurationsQuery(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IGetOrganizationIntegrationConfigurationsQuery
{
public async Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(
Guid organizationId,
Guid integrationId)
{
var integration = await integrationRepository.GetByIdAsync(integrationId);
if (integration == null || integration.OrganizationId != organizationId)
{
throw new NotFoundException();
}

var configurations = await configurationRepository.GetManyByIntegrationAsync(integrationId);
return configurations.ToList();
}
}
Loading
Loading