mirror of
https://github.com/bitwarden/server
synced 2026-01-01 16:13:33 +00:00
Add CQRS and caching support for OrganizationIntegrations (#6689)
* Add CQRS and caching support for OrganizationIntegrations * Use primary constructor for Delete command, per Claude suggestion * Fix namespace * Add XMLDoc for new commands / queries * Remove unnecessary extra call to AddExtendedCache in Startup (call in EventIntegrationsServiceCollectionExtensions handles this instead) * Alter strategy to use one cache / database call to retrieve all configurations for an event (including wildcards) * Updated README documentation to reflect updated Caching doc and updated CQRS approach
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class EventIntegrationsServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds all event integrations commands, queries, and required cache infrastructure.
|
||||
/// This method is idempotent and can be called multiple times safely.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddEventIntegrationsCommandsQueries(
|
||||
this IServiceCollection services,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
// Ensure cache is registered first - commands depend on this keyed cache.
|
||||
// This is idempotent for the same named cache, so it's safe to call.
|
||||
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
||||
|
||||
// Add all commands/queries
|
||||
services.AddOrganizationIntegrationCommandsQueries();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddScoped<ICreateOrganizationIntegrationCommand, CreateOrganizationIntegrationCommand>();
|
||||
services.TryAddScoped<IUpdateOrganizationIntegrationCommand, UpdateOrganizationIntegrationCommand>();
|
||||
services.TryAddScoped<IDeleteOrganizationIntegrationCommand, DeleteOrganizationIntegrationCommand>();
|
||||
services.TryAddScoped<IGetOrganizationIntegrationsQuery, GetOrganizationIntegrationsQuery>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.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.OrganizationIntegrations;
|
||||
|
||||
/// <summary>
|
||||
/// Command implementation for creating organization integrations with cache invalidation support.
|
||||
/// </summary>
|
||||
public class CreateOrganizationIntegrationCommand(
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]
|
||||
IFusionCache cache)
|
||||
: ICreateOrganizationIntegrationCommand
|
||||
{
|
||||
public async Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration)
|
||||
{
|
||||
var existingIntegrations = await integrationRepository
|
||||
.GetManyByOrganizationAsync(integration.OrganizationId);
|
||||
if (existingIntegrations.Any(i => i.Type == integration.Type))
|
||||
{
|
||||
throw new BadRequestException("An integration of this type already exists for this organization.");
|
||||
}
|
||||
|
||||
var created = await integrationRepository.CreateAsync(integration);
|
||||
await cache.RemoveByTagAsync(
|
||||
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||
organizationId: integration.OrganizationId,
|
||||
integrationType: integration.Type
|
||||
));
|
||||
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.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.OrganizationIntegrations;
|
||||
|
||||
/// <summary>
|
||||
/// Command implementation for deleting organization integrations with cache invalidation support.
|
||||
/// </summary>
|
||||
public class DeleteOrganizationIntegrationCommand(
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)
|
||||
: IDeleteOrganizationIntegrationCommand
|
||||
{
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
|
||||
{
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.DeleteAsync(integration);
|
||||
await cache.RemoveByTagAsync(
|
||||
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||
organizationId: organizationId,
|
||||
integrationType: integration.Type
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||
|
||||
/// <summary>
|
||||
/// Query implementation for retrieving organization integrations.
|
||||
/// </summary>
|
||||
public class GetOrganizationIntegrationsQuery(IOrganizationIntegrationRepository integrationRepository)
|
||||
: IGetOrganizationIntegrationsQuery
|
||||
{
|
||||
public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)
|
||||
{
|
||||
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
|
||||
return integrations.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command interface for creating an OrganizationIntegration.
|
||||
/// </summary>
|
||||
public interface ICreateOrganizationIntegrationCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new organization integration.
|
||||
/// </summary>
|
||||
/// <param name="integration">The OrganizationIntegration to create.</param>
|
||||
/// <returns>The created OrganizationIntegration.</returns>
|
||||
/// <exception cref="Exceptions.BadRequestException">Thrown when an integration
|
||||
/// of the same type already exists for the organization.</exception>
|
||||
Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command interface for deleting organization integrations.
|
||||
/// </summary>
|
||||
public interface IDeleteOrganizationIntegrationCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes an organization integration.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||
/// <param name="integrationId">The unique identifier of the integration to delete.</param>
|
||||
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
|
||||
/// or does not belong to the specified organization.</exception>
|
||||
Task DeleteAsync(Guid organizationId, Guid integrationId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Query interface for retrieving organization integrations.
|
||||
/// </summary>
|
||||
public interface IGetOrganizationIntegrationsQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves all organization integrations for a specific organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||
/// <returns>A list of organization integrations associated with the organization.</returns>
|
||||
Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command interface for updating organization integrations.
|
||||
/// </summary>
|
||||
public interface IUpdateOrganizationIntegrationCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates an existing organization integration.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||
/// <param name="integrationId">The unique identifier of the integration to update.</param>
|
||||
/// <param name="updatedIntegration">The updated organization integration data.</param>
|
||||
/// <returns>The updated organization integration.</returns>
|
||||
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist,
|
||||
/// does not belong to the specified organization, or the integration type does not match.</exception>
|
||||
Task<OrganizationIntegration> UpdateAsync(Guid organizationId, Guid integrationId, OrganizationIntegration updatedIntegration);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.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.OrganizationIntegrations;
|
||||
|
||||
/// <summary>
|
||||
/// Command implementation for updating organization integrations with cache invalidation support.
|
||||
/// </summary>
|
||||
public class UpdateOrganizationIntegrationCommand(
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]
|
||||
IFusionCache cache)
|
||||
: IUpdateOrganizationIntegrationCommand
|
||||
{
|
||||
public async Task<OrganizationIntegration> UpdateAsync(
|
||||
Guid organizationId,
|
||||
Guid integrationId,
|
||||
OrganizationIntegration updatedIntegration)
|
||||
{
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null ||
|
||||
integration.OrganizationId != organizationId ||
|
||||
integration.Type != updatedIntegration.Type)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
updatedIntegration.Id = integration.Id;
|
||||
updatedIntegration.OrganizationId = integration.OrganizationId;
|
||||
updatedIntegration.CreationDate = integration.CreationDate;
|
||||
await integrationRepository.ReplaceAsync(updatedIntegration);
|
||||
await cache.RemoveByTagAsync(
|
||||
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||
organizationId: organizationId,
|
||||
integrationType: integration.Type
|
||||
));
|
||||
|
||||
return updatedIntegration;
|
||||
}
|
||||
}
|
||||
@@ -296,7 +296,7 @@ graph TD
|
||||
## Caching
|
||||
|
||||
To reduce database load and improve performance, event integrations uses its own named extended cache (see
|
||||
the [README in Utilities](https://github.com/bitwarden/server/blob/main/src/Core/Utilities/README.md#extended-cache)
|
||||
[CACHING in Utilities](https://github.com/bitwarden/server/blob/main/src/Core/Utilities/CACHING.md)
|
||||
for more information). Without caching, for instance, each incoming `EventMessage` would trigger a database
|
||||
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
|
||||
|
||||
@@ -335,7 +335,8 @@ rather than using a string literal (i.e. "EventIntegrations") in code.
|
||||
- There are two places in the code that are both aware of the tagging functionality
|
||||
- The `EventIntegrationHandler` must use the tag when fetching relevant configuration details. This tells the cache
|
||||
to store the entry with the tag when it successfully loads from the repository.
|
||||
- The `OrganizationIntegrationController` needs to use the tag to remove all the tagged entries when and admin
|
||||
- The `CreateOrganizationIntegrationCommand`, `UpdateOrganizationIntegrationCommand`, and
|
||||
`DeleteOrganizationIntegrationCommand` commands need to use the tag to remove all the tagged entries when an admin
|
||||
creates, updates, or deletes an integration.
|
||||
- To ensure both places are synchronized on how to tag entries, they both use
|
||||
`EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration` to build the tag.
|
||||
|
||||
Reference in New Issue
Block a user