1
0
mirror of https://github.com/bitwarden/server synced 2026-02-06 03:33:43 +00:00

Move all event integration code to Dirt (#6757)

* Move all event integration code to Dirt

* Format to fix lint
This commit is contained in:
Brant DeBow
2025-12-30 10:59:19 -05:00
committed by GitHub
parent 9a340c0fdd
commit 86a68ab637
158 changed files with 487 additions and 472 deletions

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record DatadogIntegration(string ApiKey, Uri Uri);

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record DatadogIntegrationConfigurationDetails(string ApiKey, Uri Uri);

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class DatadogListenerConfiguration(GlobalSettings globalSettings)
: ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Datadog;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.DatadogIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.DatadogEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.DatadogIntegrationSubscriptionName;
}
}

View File

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

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class HecListenerConfiguration(GlobalSettings globalSettings)
: ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Hec;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.HecEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.HecIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.HecIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.HecEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.HecIntegrationSubscriptionName;
}
}

View File

@@ -1,10 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public interface IEventListenerConfiguration
{
public string EventQueueName { get; }
public string EventSubscriptionName { get; }
public string EventTopicName { get; }
public int EventPrefetchCount { get; }
public int EventMaxConcurrentCalls { get; }
}

View File

@@ -1,20 +0,0 @@
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public interface IIntegrationListenerConfiguration : IEventListenerConfiguration
{
public IntegrationType IntegrationType { get; }
public string IntegrationQueueName { get; }
public string IntegrationRetryQueueName { get; }
public string IntegrationSubscriptionName { get; }
public string IntegrationTopicName { get; }
public int MaxRetries { get; }
public int IntegrationPrefetchCount { get; }
public int IntegrationMaxConcurrentCalls { get; }
public string RoutingKey
{
get => IntegrationType.ToRoutingKey();
}
}

View File

@@ -1,14 +0,0 @@
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public interface IIntegrationMessage
{
IntegrationType IntegrationType { get; }
string MessageId { get; set; }
string? OrganizationId { get; set; }
int RetryCount { get; }
DateTime? DelayUntilDate { get; }
void ApplyRetry(DateTime? handlerDelayUntilDate);
string ToJson();
}

View File

@@ -1,37 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
/// <summary>
/// Categories of event integration failures used for classification and retry logic.
/// </summary>
public enum IntegrationFailureCategory
{
/// <summary>
/// Service is temporarily unavailable (503, upstream outage, maintenance).
/// </summary>
ServiceUnavailable,
/// <summary>
/// Authentication failed (401, 403, invalid_auth, token issues).
/// </summary>
AuthenticationFailed,
/// <summary>
/// Configuration error (invalid config, channel_not_found, etc.).
/// </summary>
ConfigurationError,
/// <summary>
/// Rate limited (429, rate_limited).
/// </summary>
RateLimited,
/// <summary>
/// Transient error (timeouts, 500, network errors).
/// </summary>
TransientError,
/// <summary>
/// Permanent failure unrelated to authentication/config (e.g., unrecoverable payload/format issue).
/// </summary>
PermanentFailure
}

View File

@@ -1,8 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationFilterGroup
{
public bool AndOperator { get; init; } = true;
public List<IntegrationFilterRule>? Rules { get; init; }
public List<IntegrationFilterGroup>? Groups { get; init; }
}

View File

@@ -1,9 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public enum IntegrationFilterOperation
{
Equals = 0,
NotEquals = 1,
In = 2,
NotIn = 3
}

View File

@@ -1,9 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationFilterRule
{
public required string Property { get; set; }
public required IntegrationFilterOperation Operation { get; set; }
public required object? Value { get; set; }
}

View File

@@ -1,84 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
/// <summary>
/// Represents the result of an integration handler operation, including success status,
/// failure categorization, and retry metadata. Use the <see cref="Succeed"/> factory method
/// for successful operations or <see cref="Fail"/> for failures with automatic retry-ability
/// determination based on the failure category.
/// </summary>
public class IntegrationHandlerResult
{
/// <summary>
/// True if the integration send succeeded, false otherwise.
/// </summary>
public bool Success { get; }
/// <summary>
/// The integration message that was processed.
/// </summary>
public IIntegrationMessage Message { get; }
/// <summary>
/// Optional UTC date/time indicating when a failed operation should be retried.
/// Will be used by the retry queue to delay re-sending the message.
/// Usually set based on the Retry-After header from rate-limited responses.
/// </summary>
public DateTime? DelayUntilDate { get; private init; }
/// <summary>
/// Category of the failure. Null for successful results.
/// </summary>
public IntegrationFailureCategory? Category { get; private init; }
/// <summary>
/// Detailed failure reason or error message. Empty for successful results.
/// </summary>
public string? FailureReason { get; private init; }
/// <summary>
/// Indicates whether the operation is retryable.
/// Computed from the failure category.
/// </summary>
public bool Retryable => Category switch
{
IntegrationFailureCategory.RateLimited => true,
IntegrationFailureCategory.TransientError => true,
IntegrationFailureCategory.ServiceUnavailable => true,
IntegrationFailureCategory.AuthenticationFailed => false,
IntegrationFailureCategory.ConfigurationError => false,
IntegrationFailureCategory.PermanentFailure => false,
null => false,
_ => false
};
/// <summary>
/// Creates a successful result.
/// </summary>
public static IntegrationHandlerResult Succeed(IIntegrationMessage message)
{
return new IntegrationHandlerResult(success: true, message: message);
}
/// <summary>
/// Creates a failed result with a failure category and reason.
/// </summary>
public static IntegrationHandlerResult Fail(
IIntegrationMessage message,
IntegrationFailureCategory category,
string failureReason,
DateTime? delayUntil = null)
{
return new IntegrationHandlerResult(success: false, message: message)
{
Category = category,
FailureReason = failureReason,
DelayUntilDate = delayUntil
};
}
private IntegrationHandlerResult(bool success, IIntegrationMessage message)
{
Success = success;
Message = message;
}
}

View File

@@ -1,45 +0,0 @@
using System.Text.Json;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationMessage : IIntegrationMessage
{
public IntegrationType IntegrationType { get; set; }
public required string MessageId { get; set; }
public string? OrganizationId { get; set; }
public required string RenderedTemplate { get; set; }
public int RetryCount { get; set; } = 0;
public DateTime? DelayUntilDate { get; set; }
public void ApplyRetry(DateTime? handlerDelayUntilDate)
{
RetryCount++;
var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow;
var backoffSeconds = Math.Pow(2, RetryCount);
var jitterSeconds = Random.Shared.Next(0, 3);
DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);
}
public virtual string ToJson()
{
return JsonSerializer.Serialize(this);
}
}
public class IntegrationMessage<T> : IntegrationMessage
{
public required T Configuration { get; set; }
public override string ToJson()
{
return JsonSerializer.Serialize(this);
}
public static IntegrationMessage<T>? FromJson(string json)
{
return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);
}
}

View File

@@ -1,71 +0,0 @@
using System.Security.Cryptography;
using System.Text;
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationOAuthState
{
private const int _orgHashLength = 12;
private static readonly TimeSpan _maxAge = TimeSpan.FromMinutes(20);
public Guid IntegrationId { get; }
private DateTimeOffset Issued { get; }
private string OrganizationIdHash { get; }
private IntegrationOAuthState(Guid integrationId, string organizationIdHash, DateTimeOffset issued)
{
IntegrationId = integrationId;
OrganizationIdHash = organizationIdHash;
Issued = issued;
}
public static IntegrationOAuthState FromIntegration(OrganizationIntegration integration, TimeProvider timeProvider)
{
var integrationId = integration.Id;
var issuedUtc = timeProvider.GetUtcNow();
var organizationIdHash = ComputeOrgHash(integration.OrganizationId, issuedUtc.ToUnixTimeSeconds());
return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);
}
public static IntegrationOAuthState? FromString(string state, TimeProvider timeProvider)
{
if (string.IsNullOrWhiteSpace(state)) return null;
var parts = state.Split('.');
if (parts.Length != 3) return null;
// Verify timestamp
if (!long.TryParse(parts[2], out var unixSeconds)) return null;
var issuedUtc = DateTimeOffset.FromUnixTimeSeconds(unixSeconds);
var now = timeProvider.GetUtcNow();
var age = now - issuedUtc;
if (age > _maxAge) return null;
// Parse integration id and store org
if (!Guid.TryParse(parts[0], out var integrationId)) return null;
var organizationIdHash = parts[1];
return new IntegrationOAuthState(integrationId, organizationIdHash, issuedUtc);
}
public bool ValidateOrg(Guid orgId)
{
var expected = ComputeOrgHash(orgId, Issued.ToUnixTimeSeconds());
return expected == OrganizationIdHash;
}
public override string ToString()
{
return $"{IntegrationId}.{OrganizationIdHash}.{Issued.ToUnixTimeSeconds()}";
}
private static string ComputeOrgHash(Guid orgId, long timestamp)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes($"{orgId:N}:{timestamp}"));
return Convert.ToHexString(bytes)[.._orgHashLength];
}
}

View File

@@ -1,54 +0,0 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class IntegrationTemplateContext(EventMessage eventMessage)
{
public EventMessage Event { get; } = eventMessage;
public string DomainName => Event.DomainName;
public string IpAddress => Event.IpAddress;
public DeviceType? DeviceType => Event.DeviceType;
public Guid? ActingUserId => Event.ActingUserId;
public Guid? OrganizationUserId => Event.OrganizationUserId;
public DateTime Date => Event.Date;
public EventType Type => Event.Type;
public Guid? UserId => Event.UserId;
public Guid? OrganizationId => Event.OrganizationId;
public Guid? CipherId => Event.CipherId;
public Guid? CollectionId => Event.CollectionId;
public Guid? GroupId => Event.GroupId;
public Guid? PolicyId => Event.PolicyId;
public Guid? IdempotencyId => Event.IdempotencyId;
public Guid? ProviderId => Event.ProviderId;
public Guid? ProviderUserId => Event.ProviderUserId;
public Guid? ProviderOrganizationId => Event.ProviderOrganizationId;
public Guid? InstallationId => Event.InstallationId;
public Guid? SecretId => Event.SecretId;
public Guid? ProjectId => Event.ProjectId;
public Guid? ServiceAccountId => Event.ServiceAccountId;
public Guid? GrantedServiceAccountId => Event.GrantedServiceAccountId;
public string DateIso8601 => Date.ToString("o");
public string EventMessage => JsonSerializer.Serialize(Event);
public OrganizationUserUserDetails? User { get; set; }
public string? UserName => User?.Name;
public string? UserEmail => User?.Email;
public OrganizationUserType? UserType => User?.Type;
public OrganizationUserUserDetails? ActingUser { get; set; }
public string? ActingUserName => ActingUser?.Name;
public string? ActingUserEmail => ActingUser?.Email;
public OrganizationUserType? ActingUserType => ActingUser?.Type;
public Group? Group { get; set; }
public string? GroupName => Group?.Name;
public Organization? Organization { get; set; }
public string? OrganizationName => Organization?.DisplayName();
}

View File

@@ -1,48 +0,0 @@
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public abstract class ListenerConfiguration
{
protected GlobalSettings _globalSettings;
public ListenerConfiguration(GlobalSettings globalSettings)
{
_globalSettings = globalSettings;
}
public int MaxRetries
{
get => _globalSettings.EventLogging.MaxRetries;
}
public string EventTopicName
{
get => _globalSettings.EventLogging.AzureServiceBus.EventTopicName;
}
public string IntegrationTopicName
{
get => _globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName;
}
public int EventPrefetchCount
{
get => _globalSettings.EventLogging.AzureServiceBus.DefaultPrefetchCount;
}
public int EventMaxConcurrentCalls
{
get => _globalSettings.EventLogging.AzureServiceBus.DefaultMaxConcurrentCalls;
}
public int IntegrationPrefetchCount
{
get => _globalSettings.EventLogging.AzureServiceBus.DefaultPrefetchCount;
}
public int IntegrationMaxConcurrentCalls
{
get => _globalSettings.EventLogging.AzureServiceBus.DefaultMaxConcurrentCalls;
}
}

View File

@@ -1,17 +0,0 @@
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class RepositoryListenerConfiguration(GlobalSettings globalSettings)
: ListenerConfiguration(globalSettings), IEventListenerConfiguration
{
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName;
}
}

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegration(string Token);

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegrationConfiguration(string ChannelId);

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record SlackIntegrationConfigurationDetails(string ChannelId, string Token);

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class SlackListenerConfiguration(GlobalSettings globalSettings) :
ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Slack;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.SlackEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName;
}
}

View File

@@ -1,12 +0,0 @@
using Bit.Core.Models.Teams;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record TeamsIntegration(
string TenantId,
IReadOnlyList<TeamInfo> Teams,
string? ChannelId = null,
Uri? ServiceUrl = null)
{
public bool IsCompleted => !string.IsNullOrEmpty(ChannelId) && ServiceUrl is not null;
}

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record TeamsIntegrationConfigurationDetails(string ChannelId, Uri ServiceUrl);

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class TeamsListenerConfiguration(GlobalSettings globalSettings) :
ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Teams;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.TeamsEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.TeamsIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.TeamsEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.TeamsIntegrationSubscriptionName;
}
}

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegration(Uri Uri, string? Scheme = null, string? Token = null);

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfiguration(Uri Uri, string? Scheme = null, string? Token = null);

View File

@@ -1,3 +0,0 @@
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public record WebhookIntegrationConfigurationDetails(Uri Uri, string? Scheme = null, string? Token = null);

View File

@@ -1,38 +0,0 @@
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
public class WebhookListenerConfiguration(GlobalSettings globalSettings)
: ListenerConfiguration(globalSettings), IIntegrationListenerConfiguration
{
public IntegrationType IntegrationType
{
get => IntegrationType.Webhook;
}
public string EventQueueName
{
get => _globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName;
}
public string IntegrationQueueName
{
get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName;
}
public string IntegrationRetryQueueName
{
get => _globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName;
}
public string EventSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName;
}
public string IntegrationSubscriptionName
{
get => _globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName;
}
}

View File

@@ -1,66 +0,0 @@
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 OrganizationId { get; set; }
public Guid OrganizationIntegrationId { get; set; }
public IntegrationType IntegrationType { get; set; }
public EventType? EventType { get; set; }
public string? Configuration { get; set; }
public string? Filters { 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();
}
}
}
}