1
0
mirror of https://github.com/bitwarden/server synced 2026-02-25 00:52:57 +00:00

Add CQRS and caching support for OrganizationIntegrationConfigurations (#6690)

This commit is contained in:
Brant DeBow
2025-12-12 11:52:32 -05:00
committed by GitHub
parent 3de2f98681
commit 72c8967937
23 changed files with 1676 additions and 1105 deletions

View File

@@ -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;
}
}

View File

@@ -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
));
}
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,22 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for creating organization integration configurations.
/// </summary>
public interface ICreateOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Creates a new configuration for an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configuration">The configuration to create.</param>
/// <returns>The created configuration.</returns>
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
/// or does not belong to the specified organization.</exception>
/// <exception cref="Exceptions.BadRequestException">Thrown when the configuration or filters
/// are invalid for the integration type.</exception>
Task<OrganizationIntegrationConfiguration> CreateAsync(Guid organizationId, Guid integrationId, OrganizationIntegrationConfiguration configuration);
}

View File

@@ -0,0 +1,19 @@
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for deleting organization integration configurations.
/// </summary>
public interface IDeleteOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Deletes a configuration from an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configurationId">The unique identifier of the configuration to delete.</param>
/// <exception cref="Exceptions.NotFoundException">
/// Thrown when the integration or configuration does not exist,
/// or the integration does not belong to the specified organization,
/// or the configuration does not belong to the specified integration.</exception>
Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId);
}

View File

@@ -0,0 +1,19 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Query interface for retrieving organization integration configurations.
/// </summary>
public interface IGetOrganizationIntegrationConfigurationsQuery
{
/// <summary>
/// Retrieves all configurations for a specific organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <returns>A list of configurations associated with the integration.</returns>
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
/// or does not belong to the specified organization.</exception>
Task<List<OrganizationIntegrationConfiguration>> GetManyByIntegrationAsync(Guid organizationId, Guid integrationId);
}

View File

@@ -0,0 +1,25 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrationConfigurations.Interfaces;
/// <summary>
/// Command interface for updating organization integration configurations.
/// </summary>
public interface IUpdateOrganizationIntegrationConfigurationCommand
{
/// <summary>
/// Updates an existing configuration for an organization integration.
/// </summary>
/// <param name="organizationId">The unique identifier of the organization.</param>
/// <param name="integrationId">The unique identifier of the integration.</param>
/// <param name="configurationId">The unique identifier of the configuration to update.</param>
/// <param name="updatedConfiguration">The updated configuration data.</param>
/// <returns>The updated configuration.</returns>
/// <exception cref="Exceptions.NotFoundException">
/// Thrown when the integration or the configuration does not exist,
/// or the integration does not belong to the specified organization,
/// or the configuration does not belong to the specified integration.</exception>
/// <exception cref="Exceptions.BadRequestException">Thrown when the configuration or filters
/// are invalid for the integration type.</exception>
Task<OrganizationIntegrationConfiguration> UpdateAsync(Guid organizationId, Guid integrationId, Guid configurationId, OrganizationIntegrationConfiguration updatedConfiguration);
}

View File

@@ -0,0 +1,82 @@
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 updating organization integration configurations with validation and cache invalidation support.
/// </summary>
public class UpdateOrganizationIntegrationConfigurationCommand(
IOrganizationIntegrationRepository integrationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache,
IOrganizationIntegrationConfigurationValidator validator)
: IUpdateOrganizationIntegrationConfigurationCommand
{
public async Task<OrganizationIntegrationConfiguration> UpdateAsync(
Guid organizationId,
Guid integrationId,
Guid configurationId,
OrganizationIntegrationConfiguration updatedConfiguration)
{
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();
}
if (!validator.ValidateConfiguration(integration.Type, updatedConfiguration))
{
throw new BadRequestException($"Invalid Configuration and/or Filters for integration type {integration.Type}");
}
updatedConfiguration.Id = configuration.Id;
updatedConfiguration.CreationDate = configuration.CreationDate;
await configurationRepository.ReplaceAsync(updatedConfiguration);
// If either old or new EventType is null (wildcard), invalidate all cached results
// for the specific integration
if (configuration.EventType == null || updatedConfiguration.EventType == null)
{
// Wildcard involved - invalidate all cached results for this org/integration
await cache.RemoveByTagAsync(
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
organizationId: organizationId,
integrationType: integration.Type
));
return updatedConfiguration;
}
// Both are specific event types - invalidate specific cache entries
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: configuration.EventType.Value
));
// If event type changed, also clear the new event type's cache
if (configuration.EventType != updatedConfiguration.EventType)
{
await cache.RemoveAsync(
EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
organizationId: organizationId,
integrationType: integration.Type,
eventType: updatedConfiguration.EventType.Value
));
}
return updatedConfiguration;
}
}