mirror of
https://github.com/bitwarden/server
synced 2026-02-18 10:23:27 +00: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:
@@ -15,6 +15,8 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
[Required]
|
||||
public EventType EventType { get; set; }
|
||||
|
||||
public string? Filters { get; set; }
|
||||
|
||||
public string? Template { get; set; }
|
||||
|
||||
public bool IsValidForType(IntegrationType integrationType)
|
||||
@@ -24,9 +26,13 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
return false;
|
||||
case IntegrationType.Slack:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<SlackIntegrationConfiguration>();
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
IsConfigurationValid<SlackIntegrationConfiguration>() &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Webhook:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<WebhookIntegrationConfiguration>();
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
IsConfigurationValid<WebhookIntegrationConfiguration>() &&
|
||||
IsFiltersValid();
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -39,6 +45,7 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
OrganizationIntegrationId = organizationIntegrationId,
|
||||
Configuration = Configuration,
|
||||
Filters = Filters,
|
||||
EventType = EventType,
|
||||
Template = Template
|
||||
};
|
||||
@@ -48,6 +55,7 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
currentConfiguration.Configuration = Configuration;
|
||||
currentConfiguration.EventType = EventType;
|
||||
currentConfiguration.Filters = Filters;
|
||||
currentConfiguration.Template = Template;
|
||||
|
||||
return currentConfiguration;
|
||||
@@ -70,4 +78,22 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
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;
|
||||
CreationDate = organizationIntegrationConfiguration.CreationDate;
|
||||
EventType = organizationIntegrationConfiguration.EventType;
|
||||
Filters = organizationIntegrationConfiguration.Filters;
|
||||
Template = organizationIntegrationConfiguration.Template;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public string? Filters { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public EventType EventType { get; set; }
|
||||
public string? Template { get; set; }
|
||||
|
||||
@@ -15,5 +15,6 @@ public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
||||
public string? Template { get; set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public string? Filters { get; set; }
|
||||
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 EventType EventType { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public string? Filters { get; set; }
|
||||
public string? IntegrationConfiguration { 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.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class EventIntegrationHandler<T>(
|
||||
IntegrationType integrationType,
|
||||
IEventIntegrationPublisher eventIntegrationPublisher,
|
||||
IIntegrationFilterService integrationFilterService,
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
IOrganizationRepository organizationRepository,
|
||||
ILogger<EventIntegrationHandler<T>> logger)
|
||||
: IEventMessageHandler
|
||||
{
|
||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||
@@ -31,25 +34,47 @@ public class EventIntegrationHandler<T>(
|
||||
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
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}");
|
||||
|
||||
var message = new IntegrationMessage<T>
|
||||
try
|
||||
{
|
||||
IntegrationType = integrationType,
|
||||
MessageId = messageId.ToString(),
|
||||
Configuration = config,
|
||||
RenderedTemplate = renderedTemplate,
|
||||
RetryCount = 0,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
if (configuration.Filters is string filterJson)
|
||||
{
|
||||
// Evaluate filters - if false, then discard and do not process
|
||||
var filters = JsonSerializer.Deserialize<IntegrationFilterGroup>(filterJson)
|
||||
?? throw new InvalidOperationException($"Failed to deserialize Filters to FilterGroup");
|
||||
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
|
||||
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
|
||||
|
||||
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,
|
||||
EventType = oic.EventType,
|
||||
Configuration = oic.Configuration,
|
||||
Filters = oic.Filters,
|
||||
IntegrationConfiguration = oi.Configuration,
|
||||
Template = oic.Template
|
||||
};
|
||||
|
||||
@@ -614,9 +614,11 @@ public static class ServiceCollectionExtensions
|
||||
new EventIntegrationHandler<TConfig>(
|
||||
integrationType,
|
||||
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||
provider.GetRequiredService<IIntegrationFilterService>(),
|
||||
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||
provider.GetRequiredService<IUserRepository>(),
|
||||
provider.GetRequiredService<IOrganizationRepository>()));
|
||||
provider.GetRequiredService<IOrganizationRepository>(),
|
||||
provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()));
|
||||
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
@@ -647,6 +649,7 @@ public static class ServiceCollectionExtensions
|
||||
!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
|
||||
return services;
|
||||
|
||||
services.AddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||
services.AddSingleton<IAzureServiceBusService, AzureServiceBusService>();
|
||||
services.AddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();
|
||||
services.AddAzureServiceBusEventRepositoryListener(globalSettings);
|
||||
@@ -697,9 +700,11 @@ public static class ServiceCollectionExtensions
|
||||
new EventIntegrationHandler<TConfig>(
|
||||
integrationType,
|
||||
provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||
provider.GetRequiredService<IIntegrationFilterService>(),
|
||||
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||
provider.GetRequiredService<IUserRepository>(),
|
||||
provider.GetRequiredService<IOrganizationRepository>()));
|
||||
provider.GetRequiredService<IOrganizationRepository>(),
|
||||
provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()));
|
||||
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
@@ -730,6 +735,7 @@ public static class ServiceCollectionExtensions
|
||||
return services;
|
||||
}
|
||||
|
||||
services.AddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||
services.AddSingleton<IRabbitMqService, RabbitMqService>();
|
||||
services.AddSingleton<IEventIntegrationPublisher, RabbitMqService>();
|
||||
services.AddRabbitMqEventRepositoryListener(globalSettings);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
@Configuration VARCHAR(MAX),
|
||||
@Template VARCHAR(MAX),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Filters VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -18,7 +19,8 @@ BEGIN
|
||||
[Configuration],
|
||||
[Template],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
[RevisionDate],
|
||||
[Filters]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -28,6 +30,7 @@ BEGIN
|
||||
@Configuration,
|
||||
@Template,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
@RevisionDate,
|
||||
@Filters
|
||||
)
|
||||
END
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
@Configuration VARCHAR(MAX),
|
||||
@Template VARCHAR(MAX),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Filters VARCHAR(MAX) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -18,7 +19,8 @@ BEGIN
|
||||
[Configuration] = @Configuration,
|
||||
[Template] = @Template,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[Filters] = @Filters
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
||||
@@ -7,6 +7,7 @@ CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
|
||||
[Template] VARCHAR (MAX) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
[Filters] VARCHAR (MAX) NULL,
|
||||
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -6,7 +6,8 @@ AS
|
||||
oic.[EventType],
|
||||
oic.[Configuration],
|
||||
oi.[Configuration] AS [IntegrationConfiguration],
|
||||
oic.[Template]
|
||||
oic.[Template],
|
||||
oic.[Filters]
|
||||
FROM
|
||||
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||
INNER JOIN
|
||||
|
||||
Reference in New Issue
Block a user