mirror of
https://github.com/bitwarden/server.git
synced 2025-06-27 14:16:19 -05:00
[PM-17562] Add integration filter support (#5971)
* [PM-17562] Add integration filter support * Repond to PR feedback; Remove Date-related filters * Use tables to format the filter class descriptions * [PM-17562] Add database support for integration filters (#5988) * [PM-17562] Add database support for integration filters * Respond to PR review - fix database scripts * Further database updates; fix Filters to be last in views, stored procs, etc * Fix for missing nulls in stored procedures in main migration script * Reorder Filters to the bottom of OrganizationIntegrationConfiguration * Separate out the creation of filters from the IntegrationFilterService to IntegrationFIlterFactory * Move properties to static readonly field * Fix unit tests failing from merge --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com>
This commit is contained in:
parent
b951b38c37
commit
57cd628de8
@ -15,6 +15,8 @@ public class OrganizationIntegrationConfigurationRequestModel
|
|||||||
[Required]
|
[Required]
|
||||||
public EventType EventType { get; set; }
|
public EventType EventType { get; set; }
|
||||||
|
|
||||||
|
public string? Filters { get; set; }
|
||||||
|
|
||||||
public string? Template { get; set; }
|
public string? Template { get; set; }
|
||||||
|
|
||||||
public bool IsValidForType(IntegrationType integrationType)
|
public bool IsValidForType(IntegrationType integrationType)
|
||||||
@ -24,9 +26,13 @@ public class OrganizationIntegrationConfigurationRequestModel
|
|||||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||||
return false;
|
return false;
|
||||||
case IntegrationType.Slack:
|
case IntegrationType.Slack:
|
||||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<SlackIntegrationConfiguration>();
|
return !string.IsNullOrWhiteSpace(Template) &&
|
||||||
|
IsConfigurationValid<SlackIntegrationConfiguration>() &&
|
||||||
|
IsFiltersValid();
|
||||||
case IntegrationType.Webhook:
|
case IntegrationType.Webhook:
|
||||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<WebhookIntegrationConfiguration>();
|
return !string.IsNullOrWhiteSpace(Template) &&
|
||||||
|
IsConfigurationValid<WebhookIntegrationConfiguration>() &&
|
||||||
|
IsFiltersValid();
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@ -39,6 +45,7 @@ public class OrganizationIntegrationConfigurationRequestModel
|
|||||||
{
|
{
|
||||||
OrganizationIntegrationId = organizationIntegrationId,
|
OrganizationIntegrationId = organizationIntegrationId,
|
||||||
Configuration = Configuration,
|
Configuration = Configuration,
|
||||||
|
Filters = Filters,
|
||||||
EventType = EventType,
|
EventType = EventType,
|
||||||
Template = Template
|
Template = Template
|
||||||
};
|
};
|
||||||
@ -48,6 +55,7 @@ public class OrganizationIntegrationConfigurationRequestModel
|
|||||||
{
|
{
|
||||||
currentConfiguration.Configuration = Configuration;
|
currentConfiguration.Configuration = Configuration;
|
||||||
currentConfiguration.EventType = EventType;
|
currentConfiguration.EventType = EventType;
|
||||||
|
currentConfiguration.Filters = Filters;
|
||||||
currentConfiguration.Template = Template;
|
currentConfiguration.Template = Template;
|
||||||
|
|
||||||
return currentConfiguration;
|
return currentConfiguration;
|
||||||
@ -70,4 +78,22 @@ public class OrganizationIntegrationConfigurationRequestModel
|
|||||||
return false;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,13 @@ public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
|
|||||||
Configuration = organizationIntegrationConfiguration.Configuration;
|
Configuration = organizationIntegrationConfiguration.Configuration;
|
||||||
CreationDate = organizationIntegrationConfiguration.CreationDate;
|
CreationDate = organizationIntegrationConfiguration.CreationDate;
|
||||||
EventType = organizationIntegrationConfiguration.EventType;
|
EventType = organizationIntegrationConfiguration.EventType;
|
||||||
|
Filters = organizationIntegrationConfiguration.Filters;
|
||||||
Template = organizationIntegrationConfiguration.Template;
|
Template = organizationIntegrationConfiguration.Template;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public string? Configuration { get; set; }
|
public string? Configuration { get; set; }
|
||||||
|
public string? Filters { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; }
|
||||||
public EventType EventType { get; set; }
|
public EventType EventType { get; set; }
|
||||||
public string? Template { get; set; }
|
public string? Template { get; set; }
|
||||||
|
@ -15,5 +15,6 @@ public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
|||||||
public string? Template { get; set; }
|
public string? Template { get; set; }
|
||||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public string? Filters { get; set; }
|
||||||
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
#nullable enable
|
||||||
|
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
|
||||||
|
public enum IntegrationFilterOperation
|
||||||
|
{
|
||||||
|
Equals = 0,
|
||||||
|
NotEquals = 1,
|
||||||
|
In = 2,
|
||||||
|
NotIn = 3
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ public class OrganizationIntegrationConfigurationDetails
|
|||||||
public IntegrationType IntegrationType { get; set; }
|
public IntegrationType IntegrationType { get; set; }
|
||||||
public EventType EventType { get; set; }
|
public EventType EventType { get; set; }
|
||||||
public string? Configuration { get; set; }
|
public string? Configuration { get; set; }
|
||||||
|
public string? Filters { get; set; }
|
||||||
public string? IntegrationConfiguration { get; set; }
|
public string? IntegrationConfiguration { get; set; }
|
||||||
public string? Template { get; set; }
|
public string? Template { get; set; }
|
||||||
|
|
||||||
|
11
src/Core/AdminConsole/Services/IIntegrationFilterService.cs
Normal file
11
src/Core/AdminConsole/Services/IIntegrationFilterService.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public interface IIntegrationFilterService
|
||||||
|
{
|
||||||
|
bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message);
|
||||||
|
}
|
@ -6,15 +6,18 @@ using Bit.Core.AdminConsole.Utilities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class EventIntegrationHandler<T>(
|
public class EventIntegrationHandler<T>(
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType,
|
||||||
IEventIntegrationPublisher eventIntegrationPublisher,
|
IEventIntegrationPublisher eventIntegrationPublisher,
|
||||||
|
IIntegrationFilterService integrationFilterService,
|
||||||
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationRepository organizationRepository)
|
IOrganizationRepository organizationRepository,
|
||||||
|
ILogger<EventIntegrationHandler<T>> logger)
|
||||||
: IEventMessageHandler
|
: IEventMessageHandler
|
||||||
{
|
{
|
||||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
@ -31,25 +34,47 @@ public class EventIntegrationHandler<T>(
|
|||||||
|
|
||||||
foreach (var configuration in configurations)
|
foreach (var configuration in configurations)
|
||||||
{
|
{
|
||||||
var template = configuration.Template ?? string.Empty;
|
try
|
||||||
var context = await BuildContextAsync(eventMessage, template);
|
|
||||||
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);
|
|
||||||
var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid();
|
|
||||||
|
|
||||||
var config = configuration.MergedConfiguration.Deserialize<T>()
|
|
||||||
?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}");
|
|
||||||
|
|
||||||
var message = new IntegrationMessage<T>
|
|
||||||
{
|
{
|
||||||
IntegrationType = integrationType,
|
if (configuration.Filters is string filterJson)
|
||||||
MessageId = messageId.ToString(),
|
{
|
||||||
Configuration = config,
|
// Evaluate filters - if false, then discard and do not process
|
||||||
RenderedTemplate = renderedTemplate,
|
var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(filterJson)
|
||||||
RetryCount = 0,
|
?? throw new InvalidOperationException($"Failed to deserialize Filters to FilterGroup");
|
||||||
DelayUntilDate = null
|
if (!integrationFilterService.EvaluateFilterGroup(filters, eventMessage))
|
||||||
};
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await eventIntegrationPublisher.PublishAsync(message);
|
// Valid filter - assemble message and publish to Integration topic/exchange
|
||||||
|
var template = configuration.Template ?? string.Empty;
|
||||||
|
var context = await BuildContextAsync(eventMessage, template);
|
||||||
|
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);
|
||||||
|
var messageId = eventMessage.IdempotencyId ?? Guid.NewGuid();
|
||||||
|
var config = configuration.MergedConfiguration.Deserialize<T>()
|
||||||
|
?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name} - bad Configuration");
|
||||||
|
|
||||||
|
var message = new IntegrationMessage<T>
|
||||||
|
{
|
||||||
|
IntegrationType = integrationType,
|
||||||
|
MessageId = messageId.ToString(),
|
||||||
|
Configuration = config,
|
||||||
|
RenderedTemplate = renderedTemplate,
|
||||||
|
RetryCount = 0,
|
||||||
|
DelayUntilDate = null
|
||||||
|
};
|
||||||
|
|
||||||
|
await eventIntegrationPublisher.PublishAsync(message);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(
|
||||||
|
exception,
|
||||||
|
"Failed to publish Integration Message for {Type}, check Id {RecordId} for error in Configuration or Filters",
|
||||||
|
typeof(T).Name,
|
||||||
|
configuration.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public delegate bool IntegrationFilter(EventMessage message, object? value);
|
||||||
|
|
||||||
|
public static class IntegrationFilterFactory
|
||||||
|
{
|
||||||
|
public static IntegrationFilter BuildEqualityFilter<T>(string propertyName)
|
||||||
|
{
|
||||||
|
var param = Expression.Parameter(typeof(EventMessage), "m");
|
||||||
|
var valueParam = Expression.Parameter(typeof(object), "val");
|
||||||
|
|
||||||
|
var property = Expression.PropertyOrField(param, propertyName);
|
||||||
|
var typedVal = Expression.Convert(valueParam, typeof(T));
|
||||||
|
var body = Expression.Equal(property, typedVal);
|
||||||
|
|
||||||
|
var lambda = Expression.Lambda<Func<EventMessage, object?, bool>>(body, param, valueParam);
|
||||||
|
return new IntegrationFilter(lambda.Compile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IntegrationFilter BuildInFilter<T>(string propertyName)
|
||||||
|
{
|
||||||
|
var param = Expression.Parameter(typeof(EventMessage), "m");
|
||||||
|
var valueParam = Expression.Parameter(typeof(object), "val");
|
||||||
|
|
||||||
|
var property = Expression.PropertyOrField(param, propertyName);
|
||||||
|
|
||||||
|
var method = typeof(Enumerable)
|
||||||
|
.GetMethods()
|
||||||
|
.FirstOrDefault(m =>
|
||||||
|
m.Name == "Contains"
|
||||||
|
&& m.GetParameters().Length == 2)
|
||||||
|
?.MakeGenericMethod(typeof(T));
|
||||||
|
if (method is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Could not find Contains method.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var listType = typeof(IEnumerable<T>);
|
||||||
|
var castedList = Expression.Convert(valueParam, listType);
|
||||||
|
|
||||||
|
var containsCall = Expression.Call(method, castedList, property);
|
||||||
|
|
||||||
|
var lambda = Expression.Lambda<Func<EventMessage, object?, bool>>(containsCall, param, valueParam);
|
||||||
|
return new IntegrationFilter(lambda.Compile());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
|
public class IntegrationFilterService : IIntegrationFilterService
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IntegrationFilter> _equalsFilters = new();
|
||||||
|
private readonly Dictionary<string, IntegrationFilter> _inFilters = new();
|
||||||
|
private static readonly string[] _filterableProperties = new[]
|
||||||
|
{
|
||||||
|
"UserId",
|
||||||
|
"InstallationId",
|
||||||
|
"ProviderId",
|
||||||
|
"CipherId",
|
||||||
|
"CollectionId",
|
||||||
|
"GroupId",
|
||||||
|
"PolicyId",
|
||||||
|
"OrganizationUserId",
|
||||||
|
"ProviderUserId",
|
||||||
|
"ProviderOrganizationId",
|
||||||
|
"ActingUserId",
|
||||||
|
"SecretId",
|
||||||
|
"ServiceAccountId"
|
||||||
|
};
|
||||||
|
|
||||||
|
public IntegrationFilterService()
|
||||||
|
{
|
||||||
|
BuildFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool EvaluateFilterGroup(IntegrationFilterGroup group, EventMessage message)
|
||||||
|
{
|
||||||
|
var ruleResults = group.Rules?.Select(
|
||||||
|
rule => EvaluateRule(rule, message)
|
||||||
|
) ?? Enumerable.Empty<bool>();
|
||||||
|
var groupResults = group.Groups?.Select(
|
||||||
|
innerGroup => EvaluateFilterGroup(innerGroup, message)
|
||||||
|
) ?? Enumerable.Empty<bool>();
|
||||||
|
|
||||||
|
var results = ruleResults.Concat(groupResults);
|
||||||
|
return group.AndOperator ? results.All(r => r) : results.Any(r => r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EvaluateRule(IntegrationFilterRule rule, EventMessage message)
|
||||||
|
{
|
||||||
|
var key = rule.Property;
|
||||||
|
return rule.Operation switch
|
||||||
|
{
|
||||||
|
IntegrationFilterOperation.Equals => _equalsFilters.TryGetValue(key, out var equals) &&
|
||||||
|
equals(message, ToGuid(rule.Value)),
|
||||||
|
IntegrationFilterOperation.NotEquals => !(_equalsFilters.TryGetValue(key, out var equals) &&
|
||||||
|
equals(message, ToGuid(rule.Value))),
|
||||||
|
IntegrationFilterOperation.In => _inFilters.TryGetValue(key, out var inList) &&
|
||||||
|
inList(message, ToGuidList(rule.Value)),
|
||||||
|
IntegrationFilterOperation.NotIn => !(_inFilters.TryGetValue(key, out var inList) &&
|
||||||
|
inList(message, ToGuidList(rule.Value))),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildFilters()
|
||||||
|
{
|
||||||
|
foreach (var property in _filterableProperties)
|
||||||
|
{
|
||||||
|
_equalsFilters[property] = IntegrationFilterFactory.BuildEqualityFilter<Guid?>(property);
|
||||||
|
_inFilters[property] = IntegrationFilterFactory.BuildInFilter<Guid?>(property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid? ToGuid(object? value)
|
||||||
|
{
|
||||||
|
if (value is Guid guid)
|
||||||
|
{
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
if (value is string stringValue)
|
||||||
|
{
|
||||||
|
return Guid.Parse(stringValue);
|
||||||
|
}
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
return jsonElement.GetGuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidCastException("Could not convert value to Guid");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Guid?> ToGuidList(object? value)
|
||||||
|
{
|
||||||
|
if (value is IEnumerable<Guid?> guidList)
|
||||||
|
{
|
||||||
|
return guidList;
|
||||||
|
}
|
||||||
|
if (value is JsonElement { ValueKind: JsonValueKind.Array } jsonElement)
|
||||||
|
{
|
||||||
|
var list = new List<Guid?>();
|
||||||
|
foreach (var item in jsonElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
list.Add(ToGuid(item));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidCastException("Could not convert value to Guid[]");
|
||||||
|
}
|
||||||
|
}
|
@ -228,6 +228,52 @@ Currently, there are integrations / handlers for Slack and webhooks (as mentione
|
|||||||
- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from
|
- An array of `OrganizationIntegrationConfigurationDetails` is what the `EventIntegrationHandler` fetches from
|
||||||
the database to determine what to publish at the integration level.
|
the database to determine what to publish at the integration level.
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
In addition to the ability to configure integrations mentioned above, organization admins can
|
||||||
|
also add `Filters` stored in the `OrganizationIntegrationConfiguration`. Filters are completely
|
||||||
|
optional and as simple or complex as organization admins want to make them. These are stored in
|
||||||
|
the database as JSON and serialized into an `IntegrationFilterGroup`. This is then passed to
|
||||||
|
the `IntegrationFilterService`, which evaluates it to a `bool`. If it's `true`, the integration
|
||||||
|
proceeds as above. If it's `false`, we ignore this event and do not route it to the integration
|
||||||
|
level.
|
||||||
|
|
||||||
|
### `IntegrationFilterGroup`
|
||||||
|
|
||||||
|
Logical AND / OR grouping of a number of rules and other subgroups.
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `AndOperator` | Indicates whether **all** (`true`) or **any** (`false`) of the `Rules` and `Groups` must be true. This applies to _both_ the inner group and the list of rules; for instance, if this group contained Rule1 and Rule2 as well as Group1 and Group2:<br/><br/>`true`: `Rule1 && Rule2 && Group1 && Group2`<br>`false`: `Rule1 \|\| Rule2 \|\| Group1 \|\| Group2` |
|
||||||
|
| `Rules` | A list of `IntegrationFilterRule`. Can be null or empty, in which case it will return `true`. |
|
||||||
|
| `Groups` | A list of nested `IntegrationFilterGroup`. Can be null or empty, in which case it will return `true`. |
|
||||||
|
|
||||||
|
### `IntegrationFilterRule`
|
||||||
|
|
||||||
|
The core of the filtering framework to determine if the data in this specific EventMessage
|
||||||
|
matches the data for which the filter is searching.
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `Property` | The property on `EventMessage` to evaluate (e.g., `CollectionId`). |
|
||||||
|
| `Operation` | The comparison to perform between the property and `Value`. <br><br>**Supported operations:**<br>• `Equals`: `Guid` equals `Value`<br>• `NotEquals`: logical inverse of `Equals`<br>• `In`: `Guid` is in `Value` list<br>• `NotIn`: logical inverse of `In` |
|
||||||
|
| `Value` | The comparison value. Type depends on `Operation`: <br>• `Equals`, `NotEquals`: `Guid`<br>• `In`, `NotIn`: list of `Guid` |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[IntegrationFilterGroup]
|
||||||
|
A -->|Has 0..many| B1[IntegrationFilterRule]
|
||||||
|
A --> D1[And Operator]
|
||||||
|
A -->|Has 0..many| C1[Nested IntegrationFilterGroup]
|
||||||
|
|
||||||
|
B1 --> B2[Property: string]
|
||||||
|
B1 --> B3[Operation: Equals/In/DateBefore/DateAfter]
|
||||||
|
B1 --> B4[Value: object?]
|
||||||
|
|
||||||
|
C1 -->|Has many| B1_2[IntegrationFilterRule]
|
||||||
|
C1 -->|Can contain| C2[IntegrationFilterGroup...]
|
||||||
|
```
|
||||||
|
|
||||||
# Building a new integration
|
# Building a new integration
|
||||||
|
|
||||||
These are all the pieces required in the process of building out a new integration. For
|
These are all the pieces required in the process of building out a new integration. For
|
||||||
|
@ -31,6 +31,7 @@ public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrgan
|
|||||||
IntegrationType = oi.Type,
|
IntegrationType = oi.Type,
|
||||||
EventType = oic.EventType,
|
EventType = oic.EventType,
|
||||||
Configuration = oic.Configuration,
|
Configuration = oic.Configuration,
|
||||||
|
Filters = oic.Filters,
|
||||||
IntegrationConfiguration = oi.Configuration,
|
IntegrationConfiguration = oi.Configuration,
|
||||||
Template = oic.Template
|
Template = oic.Template
|
||||||
};
|
};
|
||||||
|
@ -614,9 +614,11 @@ public static class ServiceCollectionExtensions
|
|||||||
new EventIntegrationHandler<TConfig>(
|
new EventIntegrationHandler<TConfig>(
|
||||||
integrationType,
|
integrationType,
|
||||||
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
|
provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
provider.GetRequiredService<IUserRepository>(),
|
provider.GetRequiredService<IUserRepository>(),
|
||||||
provider.GetRequiredService<IOrganizationRepository>()));
|
provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
|
provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()));
|
||||||
|
|
||||||
services.AddSingleton<IHostedService>(provider =>
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
new AzureServiceBusEventListenerService(
|
new AzureServiceBusEventListenerService(
|
||||||
@ -647,6 +649,7 @@ public static class ServiceCollectionExtensions
|
|||||||
!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
|
!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
|
||||||
return services;
|
return services;
|
||||||
|
|
||||||
|
services.AddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||||
services.AddSingleton<IAzureServiceBusService, AzureServiceBusService>();
|
services.AddSingleton<IAzureServiceBusService, AzureServiceBusService>();
|
||||||
services.AddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();
|
services.AddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();
|
||||||
services.AddAzureServiceBusEventRepositoryListener(globalSettings);
|
services.AddAzureServiceBusEventRepositoryListener(globalSettings);
|
||||||
@ -697,9 +700,11 @@ public static class ServiceCollectionExtensions
|
|||||||
new EventIntegrationHandler<TConfig>(
|
new EventIntegrationHandler<TConfig>(
|
||||||
integrationType,
|
integrationType,
|
||||||
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
|
provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
provider.GetRequiredService<IUserRepository>(),
|
provider.GetRequiredService<IUserRepository>(),
|
||||||
provider.GetRequiredService<IOrganizationRepository>()));
|
provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
|
provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()));
|
||||||
|
|
||||||
services.AddSingleton<IHostedService>(provider =>
|
services.AddSingleton<IHostedService>(provider =>
|
||||||
new RabbitMqEventListenerService(
|
new RabbitMqEventListenerService(
|
||||||
@ -730,6 +735,7 @@ public static class ServiceCollectionExtensions
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
services.AddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||||
services.AddSingleton<IRabbitMqService, RabbitMqService>();
|
services.AddSingleton<IRabbitMqService, RabbitMqService>();
|
||||||
services.AddSingleton<IEventIntegrationPublisher, RabbitMqService>();
|
services.AddSingleton<IEventIntegrationPublisher, RabbitMqService>();
|
||||||
services.AddRabbitMqEventRepositoryListener(globalSettings);
|
services.AddRabbitMqEventRepositoryListener(globalSettings);
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
@Configuration VARCHAR(MAX),
|
@Configuration VARCHAR(MAX),
|
||||||
@Template VARCHAR(MAX),
|
@Template VARCHAR(MAX),
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7)
|
@RevisionDate DATETIME2(7),
|
||||||
|
@Filters VARCHAR(MAX) = NULL
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -18,7 +19,8 @@ BEGIN
|
|||||||
[Configuration],
|
[Configuration],
|
||||||
[Template],
|
[Template],
|
||||||
[CreationDate],
|
[CreationDate],
|
||||||
[RevisionDate]
|
[RevisionDate],
|
||||||
|
[Filters]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -28,6 +30,7 @@ BEGIN
|
|||||||
@Configuration,
|
@Configuration,
|
||||||
@Template,
|
@Template,
|
||||||
@CreationDate,
|
@CreationDate,
|
||||||
@RevisionDate
|
@RevisionDate,
|
||||||
|
@Filters
|
||||||
)
|
)
|
||||||
END
|
END
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
@Configuration VARCHAR(MAX),
|
@Configuration VARCHAR(MAX),
|
||||||
@Template VARCHAR(MAX),
|
@Template VARCHAR(MAX),
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7)
|
@RevisionDate DATETIME2(7),
|
||||||
|
@Filters VARCHAR(MAX) = NULL
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -18,7 +19,8 @@ BEGIN
|
|||||||
[Configuration] = @Configuration,
|
[Configuration] = @Configuration,
|
||||||
[Template] = @Template,
|
[Template] = @Template,
|
||||||
[CreationDate] = @CreationDate,
|
[CreationDate] = @CreationDate,
|
||||||
[RevisionDate] = @RevisionDate
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[Filters] = @Filters
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
END
|
END
|
||||||
|
@ -7,6 +7,7 @@ CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
|
|||||||
[Template] VARCHAR (MAX) NULL,
|
[Template] VARCHAR (MAX) NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[Filters] VARCHAR (MAX) NULL,
|
||||||
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) ON DELETE CASCADE
|
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,8 @@ AS
|
|||||||
oic.[EventType],
|
oic.[EventType],
|
||||||
oic.[Configuration],
|
oic.[Configuration],
|
||||||
oi.[Configuration] AS [IntegrationConfiguration],
|
oi.[Configuration] AS [IntegrationConfiguration],
|
||||||
oic.[Template]
|
oic.[Template],
|
||||||
|
oic.[Filters]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationIntegrationConfiguration] oic
|
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
|
@ -154,6 +154,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
||||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||||
|
|
||||||
@ -191,6 +192,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||||
|
|
||||||
@ -228,6 +230,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
||||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||||
|
|
||||||
@ -433,6 +436,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
||||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||||
|
|
||||||
@ -476,6 +480,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||||
|
|
||||||
@ -518,6 +523,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
||||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||||
|
|
||||||
@ -558,6 +564,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
|||||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||||
model.Template = "Template String";
|
model.Template = "Template String";
|
||||||
|
model.Filters = null;
|
||||||
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
@ -65,6 +65,21 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
|||||||
Assert.False(model.IsValidForType(IntegrationType.Webhook));
|
Assert.False(model.IsValidForType(IntegrationType.Webhook));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidForType_InvalidJsonFilters_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
|
||||||
|
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||||
|
{
|
||||||
|
Configuration = config,
|
||||||
|
Filters = "{Not valid json",
|
||||||
|
Template = "template"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(model.IsValidForType(IntegrationType.Webhook));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsValidForType_ScimIntegration_ReturnsFalse()
|
public void IsValidForType_ScimIntegration_ReturnsFalse()
|
||||||
{
|
{
|
||||||
@ -91,6 +106,33 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
|||||||
Assert.True(model.IsValidForType(IntegrationType.Slack));
|
Assert.True(model.IsValidForType(IntegrationType.Slack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidForType_ValidSlackConfigurationWithFilters_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
|
||||||
|
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules = [
|
||||||
|
new IntegrationFilterRule()
|
||||||
|
{
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Property = "CollectionId",
|
||||||
|
Value = Guid.NewGuid()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Groups = []
|
||||||
|
});
|
||||||
|
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||||
|
{
|
||||||
|
Configuration = config,
|
||||||
|
Filters = filters,
|
||||||
|
Template = "template"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(model.IsValidForType(IntegrationType.Slack));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
|
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
|
||||||
{
|
{
|
||||||
@ -117,6 +159,33 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
|||||||
Assert.True(model.IsValidForType(IntegrationType.Webhook));
|
Assert.True(model.IsValidForType(IntegrationType.Webhook));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValidForType_ValidWebhookConfigurationWithFilters_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
|
||||||
|
var filters = JsonSerializer.Serialize(new IntegrationFilterGroup()
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules = [
|
||||||
|
new IntegrationFilterRule()
|
||||||
|
{
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Property = "CollectionId",
|
||||||
|
Value = Guid.NewGuid()
|
||||||
|
}
|
||||||
|
],
|
||||||
|
Groups = []
|
||||||
|
});
|
||||||
|
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||||
|
{
|
||||||
|
Configuration = config,
|
||||||
|
Filters = filters,
|
||||||
|
Template = "template"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(model.IsValidForType(IntegrationType.Webhook));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
|
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
|
||||||
{
|
{
|
||||||
|
@ -10,6 +10,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -25,6 +26,8 @@ public class EventIntegrationHandlerTests
|
|||||||
private const string _url = "https://localhost";
|
private const string _url = "https://localhost";
|
||||||
private const string _url2 = "https://example.com";
|
private const string _url2 = "https://example.com";
|
||||||
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
||||||
|
private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =
|
||||||
|
Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();
|
||||||
|
|
||||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||||
@ -37,6 +40,7 @@ public class EventIntegrationHandlerTests
|
|||||||
.SetDependency(configurationRepository)
|
.SetDependency(configurationRepository)
|
||||||
.SetDependency(_eventIntegrationPublisher)
|
.SetDependency(_eventIntegrationPublisher)
|
||||||
.SetDependency(IntegrationType.Webhook)
|
.SetDependency(IntegrationType.Webhook)
|
||||||
|
.SetDependency(_logger)
|
||||||
.Create();
|
.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +86,29 @@ public class EventIntegrationHandlerTests
|
|||||||
return [config, config2];
|
return [config, config2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<OrganizationIntegrationConfigurationDetails> InvalidFilterConfiguration()
|
||||||
|
{
|
||||||
|
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||||
|
config.Configuration = null;
|
||||||
|
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
|
||||||
|
config.Template = _templateBase;
|
||||||
|
config.Filters = "Invalid Configuration!";
|
||||||
|
|
||||||
|
return [config];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<OrganizationIntegrationConfigurationDetails> ValidFilterConfiguration()
|
||||||
|
{
|
||||||
|
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||||
|
config.Configuration = null;
|
||||||
|
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
|
||||||
|
config.Template = _templateBase;
|
||||||
|
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { });
|
||||||
|
|
||||||
|
return [config];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
@ -92,7 +119,7 @@ public class EventIntegrationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
@ -109,6 +136,27 @@ public class EventIntegrationHandlerTests
|
|||||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||||
|
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||||
|
);
|
||||||
|
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||||
|
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||||
|
|
||||||
|
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2);
|
||||||
|
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||||
|
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
@ -170,6 +218,50 @@ public class EventIntegrationHandlerTests
|
|||||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||||
|
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
|
||||||
|
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||||
|
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||||
|
_logger.Received(1).Log(
|
||||||
|
LogLevel.Error,
|
||||||
|
Arg.Any<EventId>(),
|
||||||
|
Arg.Any<object>(),
|
||||||
|
Arg.Any<JsonException>(),
|
||||||
|
Arg.Any<Func<object, Exception, string>>());
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
@ -180,7 +272,7 @@ public class EventIntegrationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
@ -197,7 +289,7 @@ public class EventIntegrationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes(
|
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
||||||
List<EventMessage> eventMessages)
|
List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
public class IntegrationFilterFactoryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildEqualityFilter_ReturnsCorrectMatch(EventMessage message)
|
||||||
|
{
|
||||||
|
var different = Guid.NewGuid();
|
||||||
|
var expected = Guid.NewGuid();
|
||||||
|
message.UserId = expected;
|
||||||
|
|
||||||
|
var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>("UserId");
|
||||||
|
|
||||||
|
Assert.True(filter(message, expected));
|
||||||
|
Assert.False(filter(message, different));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildEqualityFilter_UserIdIsNull_ReturnsFalse(EventMessage message)
|
||||||
|
{
|
||||||
|
message.UserId = null;
|
||||||
|
|
||||||
|
var filter = IntegrationFilterFactory.BuildEqualityFilter<Guid?>("UserId");
|
||||||
|
|
||||||
|
Assert.False(filter(message, Guid.NewGuid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildInFilter_ReturnsCorrectMatch(EventMessage message)
|
||||||
|
{
|
||||||
|
var match = Guid.NewGuid();
|
||||||
|
message.UserId = match;
|
||||||
|
var inList = new List<Guid?> { Guid.NewGuid(), match, Guid.NewGuid() };
|
||||||
|
var outList = new List<Guid?> { Guid.NewGuid(), Guid.NewGuid() };
|
||||||
|
|
||||||
|
var filter = IntegrationFilterFactory.BuildInFilter<Guid?>("UserId");
|
||||||
|
|
||||||
|
Assert.True(filter(message, inList));
|
||||||
|
Assert.False(filter(message, outList));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,399 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
|
public class IntegrationFilterServiceTests
|
||||||
|
{
|
||||||
|
private readonly IntegrationFilterService _service = new();
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_EqualsUserId_Matches(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
eventMessage.UserId = userId;
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Value = userId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_EqualsUserId_DoesNotMatch(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.UserId = Guid.NewGuid();
|
||||||
|
var otherUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Value = otherUserId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NotEqualsUniqueUserId_ReturnsTrue(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var otherId = Guid.NewGuid();
|
||||||
|
eventMessage.UserId = otherId;
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = IntegrationFilterOperation.NotEquals,
|
||||||
|
Value = Guid.NewGuid()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NotEqualsMatchingUserId_ReturnsFalse(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
eventMessage.UserId = id;
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = IntegrationFilterOperation.NotEquals,
|
||||||
|
Value = id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_InCollectionId_Matches(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
eventMessage.CollectionId = id;
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.In,
|
||||||
|
Value = new Guid?[] { Guid.NewGuid(), id }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_InCollectionId_DoesNotMatch(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.CollectionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.In,
|
||||||
|
Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NotInCollectionIdUniqueId_ReturnsTrue(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.CollectionId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.NotIn,
|
||||||
|
Value = new Guid?[] { Guid.NewGuid(), Guid.NewGuid() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NotInCollectionIdPresent_ReturnsFalse(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var matchId = Guid.NewGuid();
|
||||||
|
eventMessage.CollectionId = matchId;
|
||||||
|
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.NotIn,
|
||||||
|
Value = new Guid?[] { Guid.NewGuid(), matchId }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(group);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.False(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NestedGroups_AllMatch(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var id = Guid.NewGuid();
|
||||||
|
var collectionId = Guid.NewGuid();
|
||||||
|
eventMessage.UserId = id;
|
||||||
|
eventMessage.CollectionId = collectionId;
|
||||||
|
|
||||||
|
var nestedGroup = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new() { Property = "UserId", Operation = IntegrationFilterOperation.Equals, Value = id },
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.In,
|
||||||
|
Value = new Guid?[] { collectionId, Guid.NewGuid() }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var topGroup = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
AndOperator = true,
|
||||||
|
Groups = [nestedGroup]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(topGroup, eventMessage);
|
||||||
|
Assert.True(result);
|
||||||
|
|
||||||
|
var jsonGroup = JsonSerializer.Serialize(topGroup);
|
||||||
|
var roundtrippedGroup = JsonSerializer.Deserialize<IntegrationFilterGroup>(jsonGroup);
|
||||||
|
Assert.NotNull(roundtrippedGroup);
|
||||||
|
Assert.True(_service.EvaluateFilterGroup(roundtrippedGroup, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_UnknownProperty_ReturnsFalse(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new() { Property = "NotARealProperty", Operation = IntegrationFilterOperation.Equals, Value = "test" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_UnsupportedOperation_ReturnsFalse(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = (IntegrationFilterOperation)999, // Unknown operation
|
||||||
|
Value = eventMessage.UserId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_WrongTypeForInList_ThrowsException(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "CollectionId",
|
||||||
|
Operation = IntegrationFilterOperation.In,
|
||||||
|
Value = "not an array" // Should be Guid[]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Throws<InvalidCastException>(() =>
|
||||||
|
_service.EvaluateFilterGroup(group, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_NullValue_ThrowsException(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "UserId",
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Value = null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Throws<InvalidCastException>(() =>
|
||||||
|
_service.EvaluateFilterGroup(group, eventMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_EmptyRuleList_ReturnsTrue(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Rules = [],
|
||||||
|
Groups = [],
|
||||||
|
AndOperator = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.True(result); // Nothing to fail, returns true by design
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void EvaluateFilterGroup_InvalidNestedGroup_ReturnsFalse(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
var group = new IntegrationFilterGroup
|
||||||
|
{
|
||||||
|
Groups =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Property = "Nope",
|
||||||
|
Operation = IntegrationFilterOperation.Equals,
|
||||||
|
Value = "bad"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
AndOperator = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _service.EvaluateFilterGroup(group, eventMessage);
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
/* add new column "Filters", nullable to OrganizationIntegrationConfiguration */
|
||||||
|
|
||||||
|
IF COL_LENGTH('[dbo].[OrganizationIntegrationConfiguration]', 'Filters') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
ADD
|
||||||
|
[Filters] VARCHAR (MAX) NULL
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* add column "Filters" to OrganizationIntegrationConfigurationDetailsView */
|
||||||
|
CREATE OR ALTER VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
oi.[OrganizationId],
|
||||||
|
oi.[Type] AS [IntegrationType],
|
||||||
|
oic.[EventType],
|
||||||
|
oic.[Configuration],
|
||||||
|
oi.[Configuration] AS [IntegrationConfiguration],
|
||||||
|
oic.[Template],
|
||||||
|
oic.[Filters]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* add column "Filters" to OrganizationIntegrationConfigurationView */
|
||||||
|
CREATE OR ALTER VIEW [dbo].[OrganizationIntegrationConfigurationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* add column to OrganizationIntegrationConfiguration_Create */
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationIntegrationId UNIQUEIDENTIFIER,
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@Configuration VARCHAR(MAX),
|
||||||
|
@Template VARCHAR(MAX),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@Filters VARCHAR(MAX) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[OrganizationIntegrationId],
|
||||||
|
[EventType],
|
||||||
|
[Configuration],
|
||||||
|
[Template],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[Filters]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@OrganizationIntegrationId,
|
||||||
|
@EventType,
|
||||||
|
@Configuration,
|
||||||
|
@Template,
|
||||||
|
@CreationDate,
|
||||||
|
@RevisionDate,
|
||||||
|
@Filters
|
||||||
|
)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* add column to OrganizationIntegrationConfiguration_Update */
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@OrganizationIntegrationId UNIQUEIDENTIFIER,
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@Configuration VARCHAR(MAX),
|
||||||
|
@Template VARCHAR(MAX),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@Filters VARCHAR(MAX) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
SET
|
||||||
|
[OrganizationIntegrationId] = @OrganizationIntegrationId,
|
||||||
|
[EventType] = @EventType,
|
||||||
|
[Configuration] = @Configuration,
|
||||||
|
[Template] = @Template,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[Filters] = @Filters
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
||||||
|
GO
|
3175
util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs
generated
Normal file
3175
util/MySqlMigrations/Migrations/20250619185012_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration");
|
||||||
|
}
|
||||||
|
}
|
@ -316,6 +316,9 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<int>("EventType")
|
b.Property<int>("EventType")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Filters")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<Guid>("OrganizationIntegrationId")
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration");
|
||||||
|
}
|
||||||
|
}
|
@ -319,6 +319,9 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<int>("EventType")
|
b.Property<int>("EventType")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Filters")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<Guid>("OrganizationIntegrationId")
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
3164
util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs
generated
Normal file
3164
util/SqliteMigrations/Migrations/20250619184959_AddFiltersToOrganizationIntegrationConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFiltersToOrganizationIntegrationConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Filters",
|
||||||
|
table: "OrganizationIntegrationConfiguration");
|
||||||
|
}
|
||||||
|
}
|
@ -311,6 +311,9 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<int>("EventType")
|
b.Property<int>("EventType")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Filters")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<Guid>("OrganizationIntegrationId")
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user