mirror of
https://github.com/bitwarden/server.git
synced 2025-06-27 14:16:19 -05:00
Merge branch 'main' into ac/pm-22101/enforce-restrictions-on-default-collection
This commit is contained in:
commit
ec8b724aeb
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -59,6 +59,8 @@ jobs:
|
||||
name: Create GitHub release
|
||||
runs-on: ubuntu-22.04
|
||||
needs: setup
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download latest release Docker stubs
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
|
@ -59,6 +59,7 @@ RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
krb5-user \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app from the build stage
|
||||
|
@ -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; }
|
||||
|
@ -4,7 +4,6 @@ using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -72,7 +71,7 @@ public class EmergencyAccessController : Controller
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var policies = await _emergencyAccessService.GetPoliciesAsync(id, user);
|
||||
var responses = policies.Select<Policy, PolicyResponseModel>(policy => new PolicyResponseModel(policy));
|
||||
var responses = policies?.Select(policy => new PolicyResponseModel(policy));
|
||||
return new ListResponseModel<PolicyResponseModel>(responses);
|
||||
}
|
||||
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
public record SlackIntegration(string token);
|
||||
public record SlackIntegration(string Token);
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
public record SlackIntegrationConfiguration(string channelId);
|
||||
public record SlackIntegrationConfiguration(string ChannelId);
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
||||
public record SlackIntegrationConfigurationDetails(string ChannelId, string Token);
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
public record WebhookIntegrationConfiguration(string url);
|
||||
public record WebhookIntegrationConfiguration(string Url, string? Scheme = null, string? Token = null);
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
public record WebhookIntegrationConfigurationDetails(string url);
|
||||
public record WebhookIntegrationConfigurationDetails(string Url, string? Scheme = null, string? Token = null);
|
||||
|
@ -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
|
||||
|
@ -11,9 +11,9 @@ public class SlackIntegrationHandler(
|
||||
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
|
||||
{
|
||||
await slackService.SendSlackMessageByChannelIdAsync(
|
||||
message.Configuration.token,
|
||||
message.Configuration.Token,
|
||||
message.RenderedTemplate,
|
||||
message.Configuration.channelId
|
||||
message.Configuration.ChannelId
|
||||
);
|
||||
|
||||
return new IntegrationHandlerResult(success: true, message: message);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
|
||||
@ -20,8 +21,16 @@ public class WebhookIntegrationHandler(
|
||||
|
||||
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
|
||||
var response = await _httpClient.PostAsync(message.Configuration.url, content);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, message.Configuration.Url);
|
||||
request.Content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
|
||||
if (!string.IsNullOrEmpty(message.Configuration.Scheme))
|
||||
{
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(
|
||||
scheme: message.Configuration.Scheme,
|
||||
parameter: message.Configuration.Token
|
||||
);
|
||||
}
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message);
|
||||
|
||||
switch (response.StatusCode)
|
||||
|
@ -97,7 +97,7 @@ public class OrganizationService : IOrganizationService
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand
|
||||
)
|
||||
)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -198,6 +198,7 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
await AdjustSeatsAsync(organization, seatAdjustment);
|
||||
}
|
||||
|
||||
if (maxAutoscaleSeats != organization.MaxAutoscaleSeats)
|
||||
{
|
||||
await UpdateAutoscalingAsync(organization, maxAutoscaleSeats);
|
||||
@ -206,7 +207,6 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
private async Task UpdateAutoscalingAsync(Organization organization, int? maxAutoscaleSeats)
|
||||
{
|
||||
|
||||
if (maxAutoscaleSeats.HasValue &&
|
||||
organization.Seats.HasValue &&
|
||||
maxAutoscaleSeats.Value < organization.Seats.Value)
|
||||
@ -228,7 +228,8 @@ public class OrganizationService : IOrganizationService
|
||||
if (plan.PasswordManager.MaxSeats.HasValue && maxAutoscaleSeats.HasValue &&
|
||||
maxAutoscaleSeats > plan.PasswordManager.MaxSeats)
|
||||
{
|
||||
throw new BadRequestException(string.Concat($"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ",
|
||||
throw new BadRequestException(string.Concat(
|
||||
$"Your plan has a seat limit of {plan.PasswordManager.MaxSeats}, ",
|
||||
$"but you have specified a max autoscale count of {maxAutoscaleSeats}.",
|
||||
"Reduce your max autoscale seat count."));
|
||||
}
|
||||
@ -249,7 +250,8 @@ public class OrganizationService : IOrganizationService
|
||||
return await AdjustSeatsAsync(organization, seatAdjustment);
|
||||
}
|
||||
|
||||
private async Task<string> AdjustSeatsAsync(Organization organization, int seatAdjustment, IEnumerable<string> ownerEmails = null)
|
||||
private async Task<string> AdjustSeatsAsync(Organization organization, int seatAdjustment,
|
||||
IEnumerable<string> ownerEmails = null)
|
||||
{
|
||||
if (organization.Seats == null)
|
||||
{
|
||||
@ -285,10 +287,11 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var additionalSeats = newSeatTotal - plan.PasswordManager.BaseSeats;
|
||||
if (plan.PasswordManager.MaxAdditionalSeats.HasValue && additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
||||
if (plan.PasswordManager.MaxAdditionalSeats.HasValue &&
|
||||
additionalSeats > plan.PasswordManager.MaxAdditionalSeats.Value)
|
||||
{
|
||||
throw new BadRequestException($"Organization plan allows a maximum of " +
|
||||
$"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats.");
|
||||
$"{plan.PasswordManager.MaxAdditionalSeats.Value} additional seats.");
|
||||
}
|
||||
|
||||
if (!organization.Seats.HasValue || organization.Seats.Value > newSeatTotal)
|
||||
@ -299,8 +302,9 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
if (organization.UseAdminSponsoredFamilies || seatCounts.Sponsored > 0)
|
||||
{
|
||||
throw new BadRequestException($"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
throw new BadRequestException(
|
||||
$"Your organization has {seatCounts.Users} members and {seatCounts.Sponsored} sponsored families. " +
|
||||
$"To decrease the seat count below {seatCounts.Total}, you must remove members or sponsorships.");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -319,7 +323,8 @@ public class OrganizationService : IOrganizationService
|
||||
organization.Seats = (short?)newSeatTotal;
|
||||
await ReplaceAndUpdateCacheAsync(organization);
|
||||
|
||||
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.Seats == organization.MaxAutoscaleSeats)
|
||||
if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue &&
|
||||
organization.Seats == organization.MaxAutoscaleSeats)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -328,7 +333,9 @@ public class OrganizationService : IOrganizationService
|
||||
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
||||
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
|
||||
}
|
||||
await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization, organization.MaxAutoscaleSeats.Value, ownerEmails);
|
||||
|
||||
await _mailService.SendOrganizationMaxSeatLimitReachedEmailAsync(organization,
|
||||
organization.MaxAutoscaleSeats.Value, ownerEmails);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -362,7 +369,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var bankAccount = customer.Sources
|
||||
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
|
||||
.FirstOrDefault(s => s is BankAccount && ((BankAccount)s).Status != "verified") as BankAccount;
|
||||
if (bankAccount == null)
|
||||
{
|
||||
throw new GatewayException("Cannot find an unverified bank account.");
|
||||
@ -389,7 +396,7 @@ public class OrganizationService : IOrganizationService
|
||||
if (anySingleOrgPolicies)
|
||||
{
|
||||
throw new BadRequestException("You may not create an organization. You belong to an organization " +
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
"which has a policy that prohibits you from being a member of any other organization.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,7 +410,7 @@ public class OrganizationService : IOrganizationService
|
||||
if (license.LicenseType != LicenseType.Organization)
|
||||
{
|
||||
throw new BadRequestException("Premium licenses cannot be applied to an organization. " +
|
||||
"Upload this license from your personal account settings page.");
|
||||
"Upload this license from your personal account settings page.");
|
||||
}
|
||||
|
||||
var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
@ -422,50 +429,11 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
await ValidateSignUpPoliciesAsync(owner.Id);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
Name = license.Name,
|
||||
BillingEmail = license.BillingEmail,
|
||||
BusinessName = license.BusinessName,
|
||||
PlanType = license.PlanType,
|
||||
Seats = license.Seats,
|
||||
MaxCollections = license.MaxCollections,
|
||||
MaxStorageGb = _globalSettings.SelfHosted ? 10240 : license.MaxStorageGb, // 10 TB
|
||||
UsePolicies = license.UsePolicies,
|
||||
UseSso = license.UseSso,
|
||||
UseKeyConnector = license.UseKeyConnector,
|
||||
UseScim = license.UseScim,
|
||||
UseGroups = license.UseGroups,
|
||||
UseDirectory = license.UseDirectory,
|
||||
UseEvents = license.UseEvents,
|
||||
UseTotp = license.UseTotp,
|
||||
Use2fa = license.Use2fa,
|
||||
UseApi = license.UseApi,
|
||||
UseResetPassword = license.UseResetPassword,
|
||||
Plan = license.Plan,
|
||||
SelfHost = license.SelfHost,
|
||||
UsersGetPremium = license.UsersGetPremium,
|
||||
UseCustomPermissions = license.UseCustomPermissions,
|
||||
Gateway = null,
|
||||
GatewayCustomerId = null,
|
||||
GatewaySubscriptionId = null,
|
||||
ReferenceData = owner.ReferenceData,
|
||||
Enabled = license.Enabled,
|
||||
ExpirationDate = license.Expires,
|
||||
LicenseKey = license.LicenseKey,
|
||||
PublicKey = publicKey,
|
||||
PrivateKey = privateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = license.UsePasswordManager,
|
||||
UseSecretsManager = license.UseSecretsManager,
|
||||
SmSeats = license.SmSeats,
|
||||
SmServiceAccounts = license.SmServiceAccounts,
|
||||
UseRiskInsights = license.UseRiskInsights,
|
||||
UseOrganizationDomains = license.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
|
||||
};
|
||||
var organization = claimsPrincipal != null
|
||||
// If the ClaimsPrincipal exists (there's a token on the license), use it to build the organization.
|
||||
? OrganizationFactory.Create(owner, claimsPrincipal, publicKey, privateKey)
|
||||
// If there's no ClaimsPrincipal (there's no token on the license), use the license to build the organization.
|
||||
: OrganizationFactory.Create(owner, license, publicKey, privateKey);
|
||||
|
||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||
|
||||
@ -480,8 +448,9 @@ public class OrganizationService : IOrganizationService
|
||||
/// Private helper method to create a new organization.
|
||||
/// This is common code used by both the cloud and self-hosted methods.
|
||||
/// </summary>
|
||||
private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,
|
||||
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
|
||||
private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)>
|
||||
SignUpAsync(Organization organization,
|
||||
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -537,7 +506,15 @@ public class OrganizationService : IOrganizationService
|
||||
if (orgUser != null)
|
||||
{
|
||||
defaultOwnerAccess =
|
||||
[new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
|
||||
[
|
||||
new CollectionAccessSelection
|
||||
{
|
||||
Id = orgUser.Id,
|
||||
HidePasswords = false,
|
||||
ReadOnly = false,
|
||||
Manage = true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
|
||||
@ -573,7 +550,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated)
|
||||
public async Task UpdateAsync(Organization organization, bool updateBilling = false,
|
||||
EventType eventType = EventType.Organization_Updated)
|
||||
{
|
||||
if (organization.Id == default(Guid))
|
||||
{
|
||||
@ -594,11 +572,12 @@ public class OrganizationService : IOrganizationService
|
||||
if (updateBilling && !string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
|
||||
{
|
||||
var customerService = new CustomerService();
|
||||
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = organization.BillingEmail,
|
||||
Description = organization.DisplayBusinessName()
|
||||
});
|
||||
await customerService.UpdateAsync(organization.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Email = organization.BillingEmail,
|
||||
Description = organization.DisplayBusinessName()
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType == EventType.Organization_CollectionManagement_Updated)
|
||||
@ -648,7 +627,8 @@ public class OrganizationService : IOrganizationService
|
||||
await UpdateAsync(organization);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId,
|
||||
EventSystemUser? systemUser,
|
||||
OrganizationUserInvite invite, string externalId)
|
||||
{
|
||||
// Ideally OrganizationUserInvite should represent a single user so that this doesn't have to be a runtime check
|
||||
@ -661,7 +641,8 @@ public class OrganizationService : IOrganizationService
|
||||
var invalidAssociations = invite.Collections?.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
||||
if (invalidAssociations?.Any() ?? false)
|
||||
{
|
||||
throw new BadRequestException("The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
|
||||
throw new BadRequestException(
|
||||
"The Manage property is mutually exclusive and cannot be true while the ReadOnly or HidePasswords properties are also true.");
|
||||
}
|
||||
|
||||
var results = await InviteUsersAsync(organizationId, invitingUserId, systemUser,
|
||||
@ -672,6 +653,7 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
throw new BadRequestException("This user has already been invited.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -683,7 +665,8 @@ public class OrganizationService : IOrganizationService
|
||||
/// <param name="systemUser">The system user which is sending the invite. Only used when inviting via SCIM; null if using a client app or Public API</param>
|
||||
/// <param name="invites">Details about the users being invited</param>
|
||||
/// <returns></returns>
|
||||
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
|
||||
EventSystemUser? systemUser,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||
{
|
||||
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
|
||||
@ -695,7 +678,8 @@ public class OrganizationService : IOrganizationService
|
||||
{
|
||||
foreach (var (invite, _) in invites)
|
||||
{
|
||||
await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null, invite.Permissions);
|
||||
await ValidateOrganizationUserUpdatePermissions(organizationId, invite.Type.Value, null,
|
||||
invite.Permissions);
|
||||
await ValidateOrganizationCustomPermissionsEnabledAsync(organizationId, invite.Type.Value);
|
||||
}
|
||||
}
|
||||
@ -705,7 +689,8 @@ public class OrganizationService : IOrganizationService
|
||||
if (systemUser.HasValue)
|
||||
{
|
||||
// Log SCIM event
|
||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser.Value, e.Item3)));
|
||||
await _eventService.LogOrganizationUserEventsAsync(events.Select(e =>
|
||||
(e.Item1, e.Item2, systemUser.Value, e.Item3)));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -716,8 +701,10 @@ public class OrganizationService : IOrganizationService
|
||||
return organizationUsers;
|
||||
}
|
||||
|
||||
private async Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||
private async
|
||||
Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)>
|
||||
SaveUsersSendInvitesAsync(Guid organizationId,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
var initialSeatCount = organization.Seats;
|
||||
@ -727,7 +714,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
|
||||
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
|
||||
organizationId, invites.SelectMany(i => i.invite.Emails), false),
|
||||
StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
// Seat autoscaling
|
||||
var initialSmSeatCount = organization.SmSeats;
|
||||
@ -755,7 +743,8 @@ public class OrganizationService : IOrganizationService
|
||||
.SelectMany(i => i.invite.Emails)
|
||||
.Count(email => !existingEmails.Contains(email));
|
||||
|
||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
|
||||
var additionalSmSeatsRequired =
|
||||
await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
@ -764,7 +753,9 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);
|
||||
if (!invitedAreAllOwners && !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true))
|
||||
if (!invitedAreAllOwners &&
|
||||
!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { },
|
||||
includeProvider: true))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
}
|
||||
@ -887,7 +878,8 @@ public class OrganizationService : IOrganizationService
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(smSubscriptionUpdateRevert);
|
||||
}
|
||||
|
||||
if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue && currentOrganization.Seats.Value != initialSeatCount.Value)
|
||||
if (initialSeatCount.HasValue && currentOrganization.Seats.HasValue &&
|
||||
currentOrganization.Seats.Value != initialSeatCount.Value)
|
||||
{
|
||||
await AdjustSeatsAsync(organization, initialSeatCount.Value - currentOrganization.Seats.Value);
|
||||
}
|
||||
@ -903,7 +895,8 @@ public class OrganizationService : IOrganizationService
|
||||
return (allOrgUsers, events);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId,
|
||||
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId,
|
||||
Guid? invitingUserId,
|
||||
IEnumerable<Guid> organizationUsersId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
@ -925,7 +918,8 @@ public class OrganizationService : IOrganizationService
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false)
|
||||
public async Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId,
|
||||
bool initOrganization = false)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
|
||||
if (orgUser == null || orgUser.OrganizationId != organizationId ||
|
||||
@ -1012,7 +1006,9 @@ public class OrganizationService : IOrganizationService
|
||||
IEnumerable<string> ownerEmails;
|
||||
if (providerOrg != null)
|
||||
{
|
||||
ownerEmails = (await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId, ProviderUserStatusType.Confirmed))
|
||||
ownerEmails =
|
||||
(await _providerUserRepository.GetManyDetailsByProviderAsync(providerOrg.ProviderId,
|
||||
ProviderUserStatusType.Confirmed))
|
||||
.Select(u => u.Email).Distinct();
|
||||
}
|
||||
else
|
||||
@ -1020,6 +1016,7 @@ public class OrganizationService : IOrganizationService
|
||||
ownerEmails = (await _organizationUserRepository.GetManyByMinimumRoleAsync(organization.Id,
|
||||
OrganizationUserType.Owner)).Select(u => u.Email).Distinct();
|
||||
}
|
||||
|
||||
var initialSeatCount = organization.Seats.Value;
|
||||
|
||||
await AdjustSeatsAsync(organization, seatsToAdd, ownerEmails);
|
||||
@ -1034,8 +1031,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
|
||||
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey,
|
||||
Guid? callingUserId)
|
||||
{
|
||||
// Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
|
||||
@ -1063,30 +1060,35 @@ public class OrganizationService : IOrganizationService
|
||||
// Block the user from withdrawal if auto enrollment is enabled
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
var resetPasswordPolicyRequirement = await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);
|
||||
var resetPasswordPolicyRequirement =
|
||||
await _policyRequirementQuery.GetAsync<ResetPasswordPolicyRequirement>(userId);
|
||||
if (resetPasswordKey == null && resetPasswordPolicyRequirement.AutoEnrollEnabled(organizationId))
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
throw new BadRequestException(
|
||||
"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resetPasswordKey == null && resetPasswordPolicy.Data != null)
|
||||
{
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data,
|
||||
JsonHelpers.IgnoreCase);
|
||||
|
||||
if (data?.AutoEnrollEnabled ?? false)
|
||||
{
|
||||
throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
throw new BadRequestException(
|
||||
"Due to an Enterprise Policy, you are not allowed to withdraw from account recovery.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orgUser.ResetPasswordKey = resetPasswordKey;
|
||||
await _organizationUserRepository.ReplaceAsync(orgUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, resetPasswordKey != null ?
|
||||
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser,
|
||||
resetPasswordKey != null
|
||||
? EventType.OrganizationUser_ResetPassword_Enroll
|
||||
: EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
}
|
||||
|
||||
public async Task ImportAsync(Guid organizationId,
|
||||
@ -1123,15 +1125,16 @@ public class OrganizationService : IOrganizationService
|
||||
var existingUsersDict = existingExternalUsers.ToDictionary(u => u.ExternalId);
|
||||
var removeUsersSet = new HashSet<string>(removeUserExternalIds)
|
||||
.Except(newUsersSet)
|
||||
.Where(u => existingUsersDict.TryGetValue(u, out var existingUser) && existingUser.Type != OrganizationUserType.Owner)
|
||||
.Where(u => existingUsersDict.TryGetValue(u, out var existingUser) &&
|
||||
existingUser.Type != OrganizationUserType.Owner)
|
||||
.Select(u => existingUsersDict[u]);
|
||||
|
||||
await _organizationUserRepository.DeleteManyAsync(removeUsersSet.Select(u => u.Id));
|
||||
events.AddRange(removeUsersSet.Select(u => (
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1144,10 +1147,10 @@ public class OrganizationService : IOrganizationService
|
||||
existingExternalUsersIdDict.ContainsKey(u.ExternalId));
|
||||
await _organizationUserRepository.DeleteManyAsync(usersToDelete.Select(u => u.Id));
|
||||
events.AddRange(usersToDelete.Select(u => (
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
u,
|
||||
EventType.OrganizationUser_Removed,
|
||||
(DateTime?)DateTime.UtcNow
|
||||
))
|
||||
);
|
||||
foreach (var deletedUser in usersToDelete)
|
||||
{
|
||||
@ -1175,6 +1178,7 @@ public class OrganizationService : IOrganizationService
|
||||
existingExternalUsersIdDict.Add(orgUser.ExternalId, orgUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationUserRepository.UpsertManyAsync(usersToUpsert);
|
||||
|
||||
// Add new users
|
||||
@ -1185,7 +1189,8 @@ public class OrganizationService : IOrganizationService
|
||||
var enoughSeatsAvailable = true;
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var seatCounts = await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var seatCounts =
|
||||
await _organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
seatsAvailable = organization.Seats.Value - seatCounts.Total;
|
||||
enoughSeatsAvailable = seatsAvailable >= usersToAdd.Count;
|
||||
}
|
||||
@ -1218,7 +1223,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser, userInvites);
|
||||
var invitedUsers = await InviteUsersAsync(organizationId, invitingUserId: null, systemUser: eventSystemUser,
|
||||
userInvites);
|
||||
foreach (var invitedUser in invitedUsers)
|
||||
{
|
||||
existingExternalUsersIdDict.Add(invitedUser.ExternalId, invitedUser.Id);
|
||||
@ -1255,7 +1261,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
|
||||
savedGroups.Select(g => (g, EventType.Group_Created, (EventSystemUser?)eventSystemUser,
|
||||
(DateTime?)DateTime.UtcNow)));
|
||||
|
||||
var updateGroups = existingExternalGroups
|
||||
.Where(g => groupsDict.ContainsKey(g.ExternalId))
|
||||
@ -1282,11 +1289,11 @@ public class OrganizationService : IOrganizationService
|
||||
await UpdateUsersAsync(group, groupsDict[group.ExternalId].ExternalUserIds,
|
||||
existingExternalUsersIdDict,
|
||||
existingGroupUsers.ContainsKey(group.Id) ? existingGroupUsers[group.Id] : null);
|
||||
|
||||
}
|
||||
|
||||
await _eventService.LogGroupEventsAsync(
|
||||
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser, (DateTime?)DateTime.UtcNow)));
|
||||
updateGroups.Select(g => (g, EventType.Group_Updated, (EventSystemUser?)eventSystemUser,
|
||||
(DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1298,10 +1305,12 @@ public class OrganizationService : IOrganizationService
|
||||
await _ssoUserRepository.DeleteAsync(userId, organizationId);
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
|
||||
var organizationUser =
|
||||
await _organizationUserRepository.GetByOrganizationAsync(organizationId.Value, userId);
|
||||
if (organizationUser != null)
|
||||
{
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UnlinkedSso);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser,
|
||||
EventType.OrganizationUser_UnlinkedSso);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1423,7 +1432,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
if ((plan.ProductTier == ProductTierType.TeamsStarter &&
|
||||
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
|
||||
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
|
||||
(plan.ProductTier != ProductTierType.TeamsStarter &&
|
||||
upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))
|
||||
{
|
||||
@ -1446,7 +1455,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType, OrganizationUserType? oldType, Permissions permissions)
|
||||
public async Task ValidateOrganizationUserUpdatePermissions(Guid organizationId, OrganizationUserType newType,
|
||||
OrganizationUserType? oldType, Permissions permissions)
|
||||
{
|
||||
if (await _currentContext.OrganizationOwner(organizationId))
|
||||
{
|
||||
@ -1473,13 +1483,15 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("Custom users can not manage Admins or Owners.");
|
||||
}
|
||||
|
||||
if (newType == OrganizationUserType.Custom && !await ValidateCustomPermissionsGrant(organizationId, permissions))
|
||||
if (newType == OrganizationUserType.Custom &&
|
||||
!await ValidateCustomPermissionsGrant(organizationId, permissions))
|
||||
{
|
||||
throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId, OrganizationUserType newType)
|
||||
public async Task ValidateOrganizationCustomPermissionsEnabledAsync(Guid organizationId,
|
||||
OrganizationUserType newType)
|
||||
{
|
||||
if (newType != OrganizationUserType.Custom)
|
||||
{
|
||||
@ -1494,7 +1506,8 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
if (!organization.UseCustomPermissions)
|
||||
{
|
||||
throw new BadRequestException("To enable custom permissions the organization must be on an Enterprise plan.");
|
||||
throw new BadRequestException(
|
||||
"To enable custom permissions the organization must be on an Enterprise plan.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1600,7 +1613,8 @@ public class OrganizationService : IOrganizationService
|
||||
EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRevokeUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, systemUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked,
|
||||
systemUser);
|
||||
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
@ -1615,7 +1629,8 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("Already revoked.");
|
||||
}
|
||||
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true))
|
||||
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId,
|
||||
new[] { organizationUser.Id }, includeProvider: true))
|
||||
{
|
||||
throw new BadRequestException("Organization must have at least one confirmed owner.");
|
||||
}
|
||||
@ -1663,7 +1678,8 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("You cannot revoke yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue && !deletingUserIsOwner)
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && revokingUserId.HasValue &&
|
||||
!deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can revoke other owners.");
|
||||
}
|
||||
|
114
src/Core/AdminConsole/Services/OrganizationFactory.cs
Normal file
114
src/Core/AdminConsole/Services/OrganizationFactory.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public static class OrganizationFactory
|
||||
{
|
||||
public static Organization Create(
|
||||
User owner,
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
string publicKey,
|
||||
string privateKey) => new()
|
||||
{
|
||||
Name = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Name),
|
||||
BillingEmail = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BillingEmail),
|
||||
BusinessName = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.BusinessName),
|
||||
PlanType = claimsPrincipal.GetValue<PlanType>(OrganizationLicenseConstants.PlanType),
|
||||
Seats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.Seats),
|
||||
MaxCollections = claimsPrincipal.GetValue<short?>(OrganizationLicenseConstants.MaxCollections),
|
||||
MaxStorageGb = 10240,
|
||||
UsePolicies = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePolicies),
|
||||
UseSso = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSso),
|
||||
UseKeyConnector = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseKeyConnector),
|
||||
UseScim = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseScim),
|
||||
UseGroups = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseGroups),
|
||||
UseDirectory = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseDirectory),
|
||||
UseEvents = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseEvents),
|
||||
UseTotp = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseTotp),
|
||||
Use2fa = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Use2fa),
|
||||
UseApi = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseApi),
|
||||
UseResetPassword = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseResetPassword),
|
||||
Plan = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.Plan),
|
||||
SelfHost = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.SelfHost),
|
||||
UsersGetPremium = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsersGetPremium),
|
||||
UseCustomPermissions =
|
||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseCustomPermissions),
|
||||
Gateway = null,
|
||||
GatewayCustomerId = null,
|
||||
GatewaySubscriptionId = null,
|
||||
ReferenceData = owner.ReferenceData,
|
||||
Enabled = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.Enabled),
|
||||
ExpirationDate = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.Expires),
|
||||
LicenseKey = claimsPrincipal.GetValue<string>(OrganizationLicenseConstants.LicenseKey),
|
||||
PublicKey = publicKey,
|
||||
PrivateKey = privateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePasswordManager),
|
||||
UseSecretsManager = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseSecretsManager),
|
||||
SmSeats = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmSeats),
|
||||
SmServiceAccounts = claimsPrincipal.GetValue<int?>(OrganizationLicenseConstants.SmServiceAccounts),
|
||||
UseRiskInsights = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseRiskInsights),
|
||||
UseOrganizationDomains =
|
||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseOrganizationDomains),
|
||||
UseAdminSponsoredFamilies =
|
||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
||||
};
|
||||
|
||||
public static Organization Create(
|
||||
User owner,
|
||||
OrganizationLicense license,
|
||||
string publicKey,
|
||||
string privateKey) => new()
|
||||
{
|
||||
Name = license.Name,
|
||||
BillingEmail = license.BillingEmail,
|
||||
BusinessName = license.BusinessName,
|
||||
PlanType = license.PlanType,
|
||||
Seats = license.Seats,
|
||||
MaxCollections = license.MaxCollections,
|
||||
MaxStorageGb = 10240,
|
||||
UsePolicies = license.UsePolicies,
|
||||
UseSso = license.UseSso,
|
||||
UseKeyConnector = license.UseKeyConnector,
|
||||
UseScim = license.UseScim,
|
||||
UseGroups = license.UseGroups,
|
||||
UseDirectory = license.UseDirectory,
|
||||
UseEvents = license.UseEvents,
|
||||
UseTotp = license.UseTotp,
|
||||
Use2fa = license.Use2fa,
|
||||
UseApi = license.UseApi,
|
||||
UseResetPassword = license.UseResetPassword,
|
||||
Plan = license.Plan,
|
||||
SelfHost = license.SelfHost,
|
||||
UsersGetPremium = license.UsersGetPremium,
|
||||
UseCustomPermissions = license.UseCustomPermissions,
|
||||
Gateway = null,
|
||||
GatewayCustomerId = null,
|
||||
GatewaySubscriptionId = null,
|
||||
ReferenceData = owner.ReferenceData,
|
||||
Enabled = license.Enabled,
|
||||
ExpirationDate = license.Expires,
|
||||
LicenseKey = license.LicenseKey,
|
||||
PublicKey = publicKey,
|
||||
PrivateKey = privateKey,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = license.UsePasswordManager,
|
||||
UseSecretsManager = license.UseSecretsManager,
|
||||
SmSeats = license.SmSeats,
|
||||
SmServiceAccounts = license.SmServiceAccounts,
|
||||
UseRiskInsights = license.UseRiskInsights,
|
||||
UseOrganizationDomains = license.UseOrganizationDomains,
|
||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
|
||||
};
|
||||
}
|
@ -34,6 +34,7 @@ public class OrganizationSale
|
||||
var subscriptionSetup = GetSubscriptionSetup(signup);
|
||||
|
||||
subscriptionSetup.SkipTrial = signup.SkipTrial;
|
||||
subscriptionSetup.InitiationPath = signup.InitiationPath;
|
||||
|
||||
return new OrganizationSale
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ public class SubscriptionSetup
|
||||
public required PasswordManager PasswordManagerOptions { get; set; }
|
||||
public SecretsManager? SecretsManagerOptions { get; set; }
|
||||
public bool SkipTrial = false;
|
||||
public string? InitiationPath { get; set; }
|
||||
|
||||
public class PasswordManager
|
||||
{
|
||||
|
@ -420,7 +420,11 @@ public class OrganizationBillingService(
|
||||
Items = subscriptionItemOptionsList,
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["organizationId"] = organizationId.ToString()
|
||||
["organizationId"] = organizationId.ToString(),
|
||||
["trialInitiationPath"] = !string.IsNullOrEmpty(subscriptionSetup.InitiationPath) &&
|
||||
subscriptionSetup.InitiationPath.Contains("trial from marketing website")
|
||||
? "marketing-initiated"
|
||||
: "product-initiated"
|
||||
},
|
||||
OffSession = true,
|
||||
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
|
||||
|
@ -194,7 +194,6 @@ public static class FeatureFlagKeys
|
||||
public const string IpcChannelFramework = "ipc-channel-framework";
|
||||
|
||||
/* Tools Team */
|
||||
public const string ItemShare = "item-share";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
|
||||
/* Vault Team */
|
||||
|
@ -59,7 +59,7 @@
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.7.0" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.9.1" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||
|
@ -50,6 +50,7 @@ RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
krb5-user \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy app from the build stage
|
||||
|
@ -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
|
||||
|
@ -151,9 +151,10 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||
|
||||
@ -188,9 +189,48 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_OnlyUrlProvided_Webhook_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||
|
||||
@ -350,7 +390,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = null;
|
||||
|
||||
@ -393,9 +433,10 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||
|
||||
@ -436,9 +477,53 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_OnlyUrlProvided_Webhook_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||
|
||||
@ -476,9 +561,10 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(Url: "https://localhost", Scheme: "Bearer", Token: "AUTH-TOKEN");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
model.Filters = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
@ -582,7 +668,7 @@ public class OrganizationIntegrationsConfigurationControllerTests
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
var slackConfig = new SlackIntegrationConfiguration(ChannelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = null;
|
||||
|
||||
|
@ -43,7 +43,7 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
[InlineData(" ")]
|
||||
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
@ -65,6 +65,21 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
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]
|
||||
public void IsValidForType_ScimIntegration_ReturnsFalse()
|
||||
{
|
||||
@ -92,7 +107,34 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
|
||||
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]
|
||||
public void IsValidForType_ValidNoAuthWebhookConfiguration_ReturnsTrue()
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
@ -104,6 +146,46 @@ public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
Assert.True(model.IsValidForType(IntegrationType.Webhook));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com", "Bearer", "AUTH-TOKEN"));
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
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]
|
||||
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ public class IntegrationMessageTests
|
||||
{
|
||||
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = _messageId,
|
||||
RetryCount = 2,
|
||||
RenderedTemplate = string.Empty,
|
||||
@ -34,7 +34,7 @@ public class IntegrationMessageTests
|
||||
{
|
||||
var message = new IntegrationMessage<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = _messageId,
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
|
@ -10,6 +10,7 @@ using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -25,6 +26,8 @@ public class EventIntegrationHandlerTests
|
||||
private const string _url = "https://localhost";
|
||||
private const string _url2 = "https://example.com";
|
||||
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
||||
private readonly ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> _logger =
|
||||
Substitute.For<ILogger<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>>();
|
||||
|
||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||
@ -37,6 +40,7 @@ public class EventIntegrationHandlerTests
|
||||
.SetDependency(configurationRepository)
|
||||
.SetDependency(_eventIntegrationPublisher)
|
||||
.SetDependency(IntegrationType.Webhook)
|
||||
.SetDependency(_logger)
|
||||
.Create();
|
||||
}
|
||||
|
||||
@ -62,7 +66,7 @@ public class EventIntegrationHandlerTests
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
|
||||
config.Template = template;
|
||||
|
||||
return [config];
|
||||
@ -72,16 +76,39 @@ public class EventIntegrationHandlerTests
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url });
|
||||
config.Template = template;
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = null;
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 });
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { Url = _url2 });
|
||||
config2.Template = template;
|
||||
|
||||
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]
|
||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
@ -92,7 +119,7 @@ public class EventIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage)
|
||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||
|
||||
@ -109,6 +136,27 @@ public class EventIntegrationHandlerTests
|
||||
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]
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||
{
|
||||
@ -180,7 +272,7 @@ public class EventIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[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));
|
||||
|
||||
@ -197,7 +289,7 @@ public class EventIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes(
|
||||
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
||||
List<EventMessage> eventMessages)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ public class IntegrationHandlerTests
|
||||
var sut = new TestIntegrationHandler();
|
||||
var expected = new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost", "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = "TestMessageId",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RenderedTemplate = "Template",
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@ -16,6 +17,8 @@ public class WebhookIntegrationHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string _scheme = "Bearer";
|
||||
private const string _token = "AUTH_TOKEN";
|
||||
private const string _webhookUrl = "http://localhost/test/event";
|
||||
|
||||
public WebhookIntegrationHandlerTests()
|
||||
@ -39,7 +42,7 @@ public class WebhookIntegrationHandlerTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
public async Task HandleAsync_SuccessfulRequestWithoutAuth_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
@ -59,6 +62,33 @@ public class WebhookIntegrationHandlerTests
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Null(request.Headers.Authorization);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequestWithAuthorizationHeader_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(new AuthenticationHeaderValue(_scheme, _token), request.Headers.Authorization);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
|
||||
}
|
||||
@ -71,7 +101,7 @@ public class WebhookIntegrationHandlerTests
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
|
||||
sutProvider.GetDependency<FakeTimeProvider>().SetUtcNow(now);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
@ -94,7 +124,7 @@ public class WebhookIntegrationHandlerTests
|
||||
var sutProvider = GetSutProvider();
|
||||
var now = new DateTime(2014, 3, 2, 1, 0, 0, DateTimeKind.Utc);
|
||||
var retryAfter = now.AddSeconds(60);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
@ -115,7 +145,7 @@ public class WebhookIntegrationHandlerTests
|
||||
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.InternalServerError)
|
||||
@ -134,7 +164,7 @@ public class WebhookIntegrationHandlerTests
|
||||
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl, _scheme, _token);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
|
||||
|
@ -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")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Filters")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<Guid>("OrganizationIntegrationId")
|
||||
.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")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Filters")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("OrganizationIntegrationId")
|
||||
.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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Filters")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("OrganizationIntegrationId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user