mirror of
https://github.com/bitwarden/server.git
synced 2025-12-12 18:30:24 -06:00
Merge branch 'main' into jmccannon/ac/pm-27131-auto-confirm-req
This commit is contained in:
commit
593c6a5ac5
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@ -44,6 +44,7 @@
|
|||||||
{
|
{
|
||||||
matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"],
|
matchPackageNames: ["https://github.com/bitwarden/sdk-internal.git"],
|
||||||
groupName: "sdk-internal",
|
groupName: "sdk-internal",
|
||||||
|
dependencyDashboardApproval: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchManagers: ["dockerfile", "docker-compose"],
|
matchManagers: ["dockerfile", "docker-compose"],
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|||||||
@ -25,6 +25,12 @@
|
|||||||
"connectionString": "UseDevelopmentStorage=true"
|
"connectionString": "UseDevelopmentStorage=true"
|
||||||
},
|
},
|
||||||
"developmentDirectory": "../../../dev",
|
"developmentDirectory": "../../../dev",
|
||||||
"pricingUri": "https://billingpricing.qa.bitwarden.pw"
|
"pricingUri": "https://billingpricing.qa.bitwarden.pw",
|
||||||
|
"mail": {
|
||||||
|
"smtp": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 10250
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,11 @@
|
|||||||
"mail": {
|
"mail": {
|
||||||
"sendGridApiKey": "SECRET",
|
"sendGridApiKey": "SECRET",
|
||||||
"amazonConfigSetName": "Email",
|
"amazonConfigSetName": "Email",
|
||||||
"replyToEmail": "no-reply@bitwarden.com"
|
"replyToEmail": "no-reply@bitwarden.com",
|
||||||
|
"smtp": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 10250
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"identityServer": {
|
"identityServer": {
|
||||||
"certificateThumbprint": "SECRET"
|
"certificateThumbprint": "SECRET"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -12,7 +12,10 @@ namespace Bit.Api.AdminConsole.Controllers;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class OrganizationIntegrationController(
|
public class OrganizationIntegrationController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IOrganizationIntegrationRepository integrationRepository) : Controller
|
ICreateOrganizationIntegrationCommand createCommand,
|
||||||
|
IUpdateOrganizationIntegrationCommand updateCommand,
|
||||||
|
IDeleteOrganizationIntegrationCommand deleteCommand,
|
||||||
|
IGetOrganizationIntegrationsQuery getQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
public async Task<List<OrganizationIntegrationResponseModel>> GetAsync(Guid organizationId)
|
public async Task<List<OrganizationIntegrationResponseModel>> GetAsync(Guid organizationId)
|
||||||
@ -22,7 +25,7 @@ public class OrganizationIntegrationController(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
|
var integrations = await getQuery.GetManyByOrganizationAsync(organizationId);
|
||||||
return integrations
|
return integrations
|
||||||
.Select(integration => new OrganizationIntegrationResponseModel(integration))
|
.Select(integration => new OrganizationIntegrationResponseModel(integration))
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -36,8 +39,10 @@ public class OrganizationIntegrationController(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var integration = await integrationRepository.CreateAsync(model.ToOrganizationIntegration(organizationId));
|
var integration = model.ToOrganizationIntegration(organizationId);
|
||||||
return new OrganizationIntegrationResponseModel(integration);
|
var created = await createCommand.CreateAsync(integration);
|
||||||
|
|
||||||
|
return new OrganizationIntegrationResponseModel(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{integrationId:guid}")]
|
[HttpPut("{integrationId:guid}")]
|
||||||
@ -48,14 +53,10 @@ public class OrganizationIntegrationController(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
var integration = model.ToOrganizationIntegration(organizationId);
|
||||||
if (integration is null || integration.OrganizationId != organizationId)
|
var updated = await updateCommand.UpdateAsync(organizationId, integrationId, integration);
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await integrationRepository.ReplaceAsync(model.ToOrganizationIntegration(integration));
|
return new OrganizationIntegrationResponseModel(updated);
|
||||||
return new OrganizationIntegrationResponseModel(integration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{integrationId:guid}")]
|
[HttpDelete("{integrationId:guid}")]
|
||||||
@ -66,13 +67,7 @@ public class OrganizationIntegrationController(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
await deleteCommand.DeleteAsync(organizationId, integrationId);
|
||||||
if (integration is null || integration.OrganizationId != organizationId)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await integrationRepository.DeleteAsync(integration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{integrationId:guid}/delete")]
|
[HttpPost("{integrationId:guid}/delete")]
|
||||||
|
|||||||
@ -41,6 +41,8 @@ using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|||||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using V1_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
|
||||||
|
using V2_RevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Controllers;
|
namespace Bit.Api.AdminConsole.Controllers;
|
||||||
|
|
||||||
@ -71,11 +73,13 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||||
|
private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;
|
||||||
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
||||||
|
private readonly V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand _revokeOrganizationUserCommandVNext;
|
||||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
private readonly V1_RevokeOrganizationUserCommand _revokeOrganizationUserCommand;
|
||||||
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
|
private readonly IAdminRecoverAccountCommand _adminRecoverAccountCommand;
|
||||||
|
|
||||||
public OrganizationUsersController(IOrganizationRepository organizationRepository,
|
public OrganizationUsersController(IOrganizationRepository organizationRepository,
|
||||||
@ -103,10 +107,12 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
V1_RevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||||
|
IBulkResendOrganizationInvitesCommand bulkResendOrganizationInvitesCommand,
|
||||||
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
||||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
|
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,
|
||||||
|
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -131,7 +137,9 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||||
|
_bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;
|
||||||
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
||||||
|
_revokeOrganizationUserCommandVNext = revokeOrganizationUserCommandVNext;
|
||||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||||
@ -273,7 +281,17 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkReinvite(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
var result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
|
|
||||||
|
IEnumerable<Tuple<Core.Entities.OrganizationUser, string>> result;
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud))
|
||||||
|
{
|
||||||
|
result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||||
|
}
|
||||||
|
|
||||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(
|
||||||
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
|
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
|
||||||
}
|
}
|
||||||
@ -628,10 +646,32 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
|||||||
[HttpPut("revoke")]
|
[HttpPut("revoke")]
|
||||||
[Authorize<ManageUsersRequirement>]
|
[Authorize<ManageUsersRequirement>]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
|
{
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2))
|
||||||
{
|
{
|
||||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentUserId = _userService.GetProperUserId(User);
|
||||||
|
if (currentUserId == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await _revokeOrganizationUserCommandVNext.RevokeUsersAsync(
|
||||||
|
new V2_RevokeOrganizationUserCommand.RevokeOrganizationUsersRequest(
|
||||||
|
orgId,
|
||||||
|
model.Ids.ToArray(),
|
||||||
|
new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))));
|
||||||
|
|
||||||
|
return new ListResponseModel<OrganizationUserBulkResponseModel>(results
|
||||||
|
.Select(result => new OrganizationUserBulkResponseModel(result.Id,
|
||||||
|
result.Result.Match(
|
||||||
|
error => error.Message,
|
||||||
|
_ => string.Empty
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPatch("revoke")]
|
[HttpPatch("revoke")]
|
||||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||||
[Authorize<ManageUsersRequirement>]
|
[Authorize<ManageUsersRequirement>]
|
||||||
|
|||||||
@ -119,7 +119,7 @@ public class OrganizationUserResetPasswordEnrollmentRequestModel
|
|||||||
|
|
||||||
public class OrganizationUserBulkRequestModel
|
public class OrganizationUserBulkRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required, MinLength(1)]
|
||||||
public IEnumerable<Guid> Ids { get; set; }
|
public IEnumerable<Guid> Ids { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -226,7 +226,8 @@ public class Startup
|
|||||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Slack / Teams Services for OAuth API requests - if configured
|
// Add Event Integrations services
|
||||||
|
services.AddEventIntegrationsCommandsQueries(globalSettings);
|
||||||
services.AddSlackService(globalSettings);
|
services.AddSlackService(globalSettings);
|
||||||
services.AddTeamsService(globalSettings);
|
services.AddTeamsService(globalSettings);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities;
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
public class OrganizationIntegration : ITableObject<Guid>
|
public class OrganizationIntegration : ITableObject<Guid>
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities;
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
||||||
|
|||||||
@ -0,0 +1,38 @@
|
|||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public static class EventIntegrationsServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds all event integrations commands, queries, and required cache infrastructure.
|
||||||
|
/// This method is idempotent and can be called multiple times safely.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEventIntegrationsCommandsQueries(
|
||||||
|
this IServiceCollection services,
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
// Ensure cache is registered first - commands depend on this keyed cache.
|
||||||
|
// This is idempotent for the same named cache, so it's safe to call.
|
||||||
|
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
||||||
|
|
||||||
|
// Add all commands/queries
|
||||||
|
services.AddOrganizationIntegrationCommandsQueries();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IServiceCollection AddOrganizationIntegrationCommandsQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.TryAddScoped<ICreateOrganizationIntegrationCommand, CreateOrganizationIntegrationCommand>();
|
||||||
|
services.TryAddScoped<IUpdateOrganizationIntegrationCommand, UpdateOrganizationIntegrationCommand>();
|
||||||
|
services.TryAddScoped<IDeleteOrganizationIntegrationCommand, DeleteOrganizationIntegrationCommand>();
|
||||||
|
services.TryAddScoped<IGetOrganizationIntegrationsQuery, GetOrganizationIntegrationsQuery>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command implementation for creating organization integrations with cache invalidation support.
|
||||||
|
/// </summary>
|
||||||
|
public class CreateOrganizationIntegrationCommand(
|
||||||
|
IOrganizationIntegrationRepository integrationRepository,
|
||||||
|
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]
|
||||||
|
IFusionCache cache)
|
||||||
|
: ICreateOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
public async Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration)
|
||||||
|
{
|
||||||
|
var existingIntegrations = await integrationRepository
|
||||||
|
.GetManyByOrganizationAsync(integration.OrganizationId);
|
||||||
|
if (existingIntegrations.Any(i => i.Type == integration.Type))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("An integration of this type already exists for this organization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var created = await integrationRepository.CreateAsync(integration);
|
||||||
|
await cache.RemoveByTagAsync(
|
||||||
|
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId: integration.OrganizationId,
|
||||||
|
integrationType: integration.Type
|
||||||
|
));
|
||||||
|
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command implementation for deleting organization integrations with cache invalidation support.
|
||||||
|
/// </summary>
|
||||||
|
public class DeleteOrganizationIntegrationCommand(
|
||||||
|
IOrganizationIntegrationRepository integrationRepository,
|
||||||
|
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)] IFusionCache cache)
|
||||||
|
: IDeleteOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
|
||||||
|
{
|
||||||
|
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||||
|
if (integration is null || integration.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
await integrationRepository.DeleteAsync(integration);
|
||||||
|
await cache.RemoveByTagAsync(
|
||||||
|
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integration.Type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query implementation for retrieving organization integrations.
|
||||||
|
/// </summary>
|
||||||
|
public class GetOrganizationIntegrationsQuery(IOrganizationIntegrationRepository integrationRepository)
|
||||||
|
: IGetOrganizationIntegrationsQuery
|
||||||
|
{
|
||||||
|
public async Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId)
|
||||||
|
{
|
||||||
|
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
|
||||||
|
return integrations.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command interface for creating an OrganizationIntegration.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICreateOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new organization integration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="integration">The OrganizationIntegration to create.</param>
|
||||||
|
/// <returns>The created OrganizationIntegration.</returns>
|
||||||
|
/// <exception cref="Exceptions.BadRequestException">Thrown when an integration
|
||||||
|
/// of the same type already exists for the organization.</exception>
|
||||||
|
Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command interface for deleting organization integrations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDeleteOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes an organization integration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||||
|
/// <param name="integrationId">The unique identifier of the integration to delete.</param>
|
||||||
|
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist
|
||||||
|
/// or does not belong to the specified organization.</exception>
|
||||||
|
Task DeleteAsync(Guid organizationId, Guid integrationId);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query interface for retrieving organization integrations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IGetOrganizationIntegrationsQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves all organization integrations for a specific organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||||
|
/// <returns>A list of organization integrations associated with the organization.</returns>
|
||||||
|
Task<List<OrganizationIntegration>> GetManyByOrganizationAsync(Guid organizationId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command interface for updating organization integrations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUpdateOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an existing organization integration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization.</param>
|
||||||
|
/// <param name="integrationId">The unique identifier of the integration to update.</param>
|
||||||
|
/// <param name="updatedIntegration">The updated organization integration data.</param>
|
||||||
|
/// <returns>The updated organization integration.</returns>
|
||||||
|
/// <exception cref="Exceptions.NotFoundException">Thrown when the integration does not exist,
|
||||||
|
/// does not belong to the specified organization, or the integration type does not match.</exception>
|
||||||
|
Task<OrganizationIntegration> UpdateAsync(Guid organizationId, Guid integrationId, OrganizationIntegration updatedIntegration);
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command implementation for updating organization integrations with cache invalidation support.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateOrganizationIntegrationCommand(
|
||||||
|
IOrganizationIntegrationRepository integrationRepository,
|
||||||
|
[FromKeyedServices(EventIntegrationsCacheConstants.CacheName)]
|
||||||
|
IFusionCache cache)
|
||||||
|
: IUpdateOrganizationIntegrationCommand
|
||||||
|
{
|
||||||
|
public async Task<OrganizationIntegration> UpdateAsync(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration updatedIntegration)
|
||||||
|
{
|
||||||
|
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||||
|
if (integration is null ||
|
||||||
|
integration.OrganizationId != organizationId ||
|
||||||
|
integration.Type != updatedIntegration.Type)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedIntegration.Id = integration.Id;
|
||||||
|
updatedIntegration.OrganizationId = integration.OrganizationId;
|
||||||
|
updatedIntegration.CreationDate = integration.CreationDate;
|
||||||
|
await integrationRepository.ReplaceAsync(updatedIntegration);
|
||||||
|
await cache.RemoveByTagAsync(
|
||||||
|
EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integration.Type
|
||||||
|
));
|
||||||
|
|
||||||
|
return updatedIntegration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
|
||||||
|
public class BulkResendOrganizationInvitesCommand : IBulkResendOrganizationInvitesCommand
|
||||||
|
{
|
||||||
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
|
||||||
|
private readonly ILogger<BulkResendOrganizationInvitesCommand> _logger;
|
||||||
|
|
||||||
|
public BulkResendOrganizationInvitesCommand(
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
|
||||||
|
ILogger<BulkResendOrganizationInvitesCommand> logger)
|
||||||
|
{
|
||||||
|
_organizationUserRepository = organizationUserRepository;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid? invitingUserId,
|
||||||
|
IEnumerable<Guid> organizationUsersId)
|
||||||
|
{
|
||||||
|
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||||
|
_logger.LogUserInviteStateDiagnostics(orgUsers);
|
||||||
|
|
||||||
|
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
if (org == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var validUsers = new List<OrganizationUser>();
|
||||||
|
var result = new List<Tuple<OrganizationUser, string>>();
|
||||||
|
|
||||||
|
foreach (var orgUser in orgUsers)
|
||||||
|
{
|
||||||
|
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(orgUser, "User invalid."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
validUsers.Add(orgUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validUsers.Any())
|
||||||
|
{
|
||||||
|
await _sendOrganizationInvitesCommand.SendInvitesAsync(
|
||||||
|
new SendInvitesRequest(validUsers, org));
|
||||||
|
|
||||||
|
result.AddRange(validUsers.Select(u => Tuple.Create(u, "")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
|
||||||
|
public interface IBulkResendOrganizationInvitesCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resend invites to multiple organization users in bulk.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The ID of the organization.</param>
|
||||||
|
/// <param name="invitingUserId">The ID of the user who is resending the invites.</param>
|
||||||
|
/// <param name="organizationUsersId">The IDs of the organization users to resend invites to.</param>
|
||||||
|
/// <returns>A tuple containing the OrganizationUser and an error message (empty string if successful)</returns>
|
||||||
|
Task<IEnumerable<Tuple<OrganizationUser, string>>> BulkResendInvitesAsync(
|
||||||
|
Guid organizationId,
|
||||||
|
Guid? invitingUserId,
|
||||||
|
IEnumerable<Guid> organizationUsersId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
|
|
||||||
public interface IRevokeOrganizationUserCommand
|
public interface IRevokeOrganizationUserCommand
|
||||||
{
|
{
|
||||||
@ -7,7 +7,7 @@ using Bit.Core.Platform.Push;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
|
|
||||||
public class RevokeOrganizationUserCommand(
|
public class RevokeOrganizationUserCommand(
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.AdminConsole.Utilities.v2;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public record UserAlreadyRevoked() : BadRequestError("Already revoked.");
|
||||||
|
public record CannotRevokeYourself() : BadRequestError("You cannot revoke yourself.");
|
||||||
|
public record OnlyOwnersCanRevokeOwners() : BadRequestError("Only owners can revoke other owners.");
|
||||||
|
public record MustHaveConfirmedOwner() : BadRequestError("Organization must have at least one confirmed owner.");
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public interface IRevokeOrganizationUserCommand
|
||||||
|
{
|
||||||
|
Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request);
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public interface IRevokeOrganizationUserValidator
|
||||||
|
{
|
||||||
|
Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(RevokeOrganizationUsersValidationRequest request);
|
||||||
|
}
|
||||||
@ -0,0 +1,114 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using OneOf.Types;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public class RevokeOrganizationUserCommand(
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IEventService eventService,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
IRevokeOrganizationUserValidator validator,
|
||||||
|
TimeProvider timeProvider,
|
||||||
|
ILogger<RevokeOrganizationUserCommand> logger)
|
||||||
|
: IRevokeOrganizationUserCommand
|
||||||
|
{
|
||||||
|
public async Task<IEnumerable<BulkCommandResult>> RevokeUsersAsync(RevokeOrganizationUsersRequest request)
|
||||||
|
{
|
||||||
|
var validationRequest = await CreateValidationRequestsAsync(request);
|
||||||
|
|
||||||
|
var results = await validator.ValidateAsync(validationRequest);
|
||||||
|
|
||||||
|
var validUsers = results.Where(r => r.IsValid).Select(r => r.Request).ToList();
|
||||||
|
|
||||||
|
await RevokeValidUsersAsync(validUsers);
|
||||||
|
|
||||||
|
await Task.WhenAll(
|
||||||
|
LogRevokedOrganizationUsersAsync(validUsers, request.PerformedBy),
|
||||||
|
SendPushNotificationsAsync(validUsers)
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.Select(r => r.Match(
|
||||||
|
error => new BulkCommandResult(r.Request.Id, error),
|
||||||
|
_ => new BulkCommandResult(r.Request.Id, new None())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<RevokeOrganizationUsersValidationRequest> CreateValidationRequestsAsync(
|
||||||
|
RevokeOrganizationUsersRequest request)
|
||||||
|
{
|
||||||
|
var organizationUserToRevoke = await organizationUserRepository
|
||||||
|
.GetManyAsync(request.OrganizationUserIdsToRevoke);
|
||||||
|
|
||||||
|
return new RevokeOrganizationUsersValidationRequest(
|
||||||
|
request.OrganizationId,
|
||||||
|
request.OrganizationUserIdsToRevoke,
|
||||||
|
request.PerformedBy,
|
||||||
|
organizationUserToRevoke);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RevokeValidUsersAsync(ICollection<OrganizationUser> validUsers)
|
||||||
|
{
|
||||||
|
if (validUsers.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await organizationUserRepository.RevokeManyByIdAsync(validUsers.Select(u => u.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogRevokedOrganizationUsersAsync(
|
||||||
|
ICollection<OrganizationUser> revokedUsers,
|
||||||
|
IActingUser actingUser)
|
||||||
|
{
|
||||||
|
if (revokedUsers.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
|
if (actingUser is SystemUser { SystemUserType: not null })
|
||||||
|
{
|
||||||
|
var revokeEventsWithSystem = revokedUsers
|
||||||
|
.Select(user => (user, EventType.OrganizationUser_Revoked, actingUser.SystemUserType!.Value,
|
||||||
|
(DateTime?)eventDate))
|
||||||
|
.ToList();
|
||||||
|
await eventService.LogOrganizationUserEventsAsync(revokeEventsWithSystem);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var revokeEvents = revokedUsers
|
||||||
|
.Select(user => (user, EventType.OrganizationUser_Revoked, (DateTime?)eventDate))
|
||||||
|
.ToList();
|
||||||
|
await eventService.LogOrganizationUserEventsAsync(revokeEvents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendPushNotificationsAsync(ICollection<OrganizationUser> revokedUsers)
|
||||||
|
{
|
||||||
|
var userIdsToNotify = revokedUsers
|
||||||
|
.Where(user => user.UserId.HasValue)
|
||||||
|
.Select(user => user.UserId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var userId in userIdsToNotify)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await pushNotificationService.PushSyncOrgKeysAsync(userId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Failed to send push notification for user {UserId}.", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public record RevokeOrganizationUsersRequest(
|
||||||
|
Guid OrganizationId,
|
||||||
|
ICollection<Guid> OrganizationUserIdsToRevoke,
|
||||||
|
IActingUser PerformedBy
|
||||||
|
);
|
||||||
|
|
||||||
|
public record RevokeOrganizationUsersValidationRequest(
|
||||||
|
Guid OrganizationId,
|
||||||
|
ICollection<Guid> OrganizationUserIdsToRevoke,
|
||||||
|
IActingUser PerformedBy,
|
||||||
|
ICollection<OrganizationUser> OrganizationUsersToRevoke
|
||||||
|
) : RevokeOrganizationUsersRequest(OrganizationId, OrganizationUserIdsToRevoke, PerformedBy);
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
public class RevokeOrganizationUsersValidator(IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||||
|
: IRevokeOrganizationUserValidator
|
||||||
|
{
|
||||||
|
public async Task<ICollection<ValidationResult<OrganizationUser>>> ValidateAsync(
|
||||||
|
RevokeOrganizationUsersValidationRequest request)
|
||||||
|
{
|
||||||
|
var hasRemainingOwner = await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(request.OrganizationId,
|
||||||
|
request.OrganizationUsersToRevoke.Select(x => x.Id) // users excluded because they are going to be revoked
|
||||||
|
);
|
||||||
|
|
||||||
|
return request.OrganizationUsersToRevoke.Select(x =>
|
||||||
|
{
|
||||||
|
return x switch
|
||||||
|
{
|
||||||
|
_ when request.PerformedBy is not SystemUser
|
||||||
|
&& x.UserId is not null
|
||||||
|
&& x.UserId == request.PerformedBy.UserId =>
|
||||||
|
Invalid(x, new CannotRevokeYourself()),
|
||||||
|
{ Status: OrganizationUserStatusType.Revoked } =>
|
||||||
|
Invalid(x, new UserAlreadyRevoked()),
|
||||||
|
{ Type: OrganizationUserType.Owner } when !hasRemainingOwner =>
|
||||||
|
Invalid(x, new MustHaveConfirmedOwner()),
|
||||||
|
{ Type: OrganizationUserType.Owner } when !request.PerformedBy.IsOrganizationOwnerOrProvider =>
|
||||||
|
Invalid(x, new OnlyOwnersCanRevokeOwners()),
|
||||||
|
|
||||||
|
_ => Valid(x)
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,10 +6,23 @@ namespace Bit.Core.Repositories;
|
|||||||
|
|
||||||
public interface IOrganizationIntegrationConfigurationRepository : IRepository<OrganizationIntegrationConfiguration, Guid>
|
public interface IOrganizationIntegrationConfigurationRepository : IRepository<OrganizationIntegrationConfiguration, Guid>
|
||||||
{
|
{
|
||||||
Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
/// <summary>
|
||||||
|
/// Retrieve the list of available configuration details for a specific event for the organization and
|
||||||
|
/// integration type.<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// <b>Note:</b> This returns all configurations that match the event type explicitly <b>and</b>
|
||||||
|
/// all the configurations that have a null event type - null event type is considered a
|
||||||
|
/// wildcard that matches all events.
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventType">The specific event type</param>
|
||||||
|
/// <param name="organizationId">The id of the organization</param>
|
||||||
|
/// <param name="integrationType">The integration type</param>
|
||||||
|
/// <returns>A List of <see cref="OrganizationIntegrationConfigurationDetails"/> that match</returns>
|
||||||
|
Task<List<OrganizationIntegrationConfigurationDetails>> GetManyByEventTypeOrganizationIdIntegrationType(
|
||||||
|
EventType eventType,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType);
|
||||||
EventType eventType);
|
|
||||||
|
|
||||||
Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();
|
Task<List<OrganizationIntegrationConfigurationDetails>> GetAllConfigurationDetailsAsync();
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Utilities;
|
using Bit.Core.AdminConsole.Utilities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -17,8 +18,8 @@ public class EventIntegrationHandler<T>(
|
|||||||
IntegrationType integrationType,
|
IntegrationType integrationType,
|
||||||
IEventIntegrationPublisher eventIntegrationPublisher,
|
IEventIntegrationPublisher eventIntegrationPublisher,
|
||||||
IIntegrationFilterService integrationFilterService,
|
IIntegrationFilterService integrationFilterService,
|
||||||
IIntegrationConfigurationDetailsCache configurationCache,
|
|
||||||
IFusionCache cache,
|
IFusionCache cache,
|
||||||
|
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -27,17 +28,7 @@ public class EventIntegrationHandler<T>(
|
|||||||
{
|
{
|
||||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
if (eventMessage.OrganizationId is not Guid organizationId)
|
foreach (var configuration in await GetConfigurationDetailsListAsync(eventMessage))
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var configurations = configurationCache.GetConfigurationDetails(
|
|
||||||
organizationId,
|
|
||||||
integrationType,
|
|
||||||
eventMessage.Type);
|
|
||||||
|
|
||||||
foreach (var configuration in configurations)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -64,7 +55,7 @@ public class EventIntegrationHandler<T>(
|
|||||||
{
|
{
|
||||||
IntegrationType = integrationType,
|
IntegrationType = integrationType,
|
||||||
MessageId = messageId.ToString(),
|
MessageId = messageId.ToString(),
|
||||||
OrganizationId = organizationId.ToString(),
|
OrganizationId = eventMessage.OrganizationId?.ToString(),
|
||||||
Configuration = config,
|
Configuration = config,
|
||||||
RenderedTemplate = renderedTemplate,
|
RenderedTemplate = renderedTemplate,
|
||||||
RetryCount = 0,
|
RetryCount = 0,
|
||||||
@ -132,6 +123,37 @@ public class EventIntegrationHandler<T>(
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsListAsync(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
if (eventMessage.OrganizationId is not Guid organizationId)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<OrganizationIntegrationConfigurationDetails> configurations = [];
|
||||||
|
|
||||||
|
var integrationTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId,
|
||||||
|
integrationType
|
||||||
|
);
|
||||||
|
|
||||||
|
configurations.AddRange(await cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
|
||||||
|
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integrationType,
|
||||||
|
eventType: eventMessage.Type),
|
||||||
|
factory: async _ => await configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(
|
||||||
|
eventType: eventMessage.Type,
|
||||||
|
organizationId: organizationId,
|
||||||
|
integrationType: integrationType),
|
||||||
|
options: new FusionCacheEntryOptions(
|
||||||
|
duration: EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails),
|
||||||
|
tags: [integrationTag]
|
||||||
|
));
|
||||||
|
|
||||||
|
return configurations;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>
|
private async Task<OrganizationUserUserDetails?> GetUserFromCacheAsync(Guid organizationId, Guid userId) =>
|
||||||
await cache.GetOrSetAsync<OrganizationUserUserDetails?>(
|
await cache.GetOrSetAsync<OrganizationUserUserDetails?>(
|
||||||
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),
|
key: EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(organizationId, userId),
|
||||||
|
|||||||
@ -1,83 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
|
||||||
|
|
||||||
public class IntegrationConfigurationDetailsCacheService : BackgroundService, IIntegrationConfigurationDetailsCache
|
|
||||||
{
|
|
||||||
private readonly record struct IntegrationCacheKey(Guid OrganizationId, IntegrationType IntegrationType, EventType? EventType);
|
|
||||||
private readonly IOrganizationIntegrationConfigurationRepository _repository;
|
|
||||||
private readonly ILogger<IntegrationConfigurationDetailsCacheService> _logger;
|
|
||||||
private readonly TimeSpan _refreshInterval;
|
|
||||||
private Dictionary<IntegrationCacheKey, List<OrganizationIntegrationConfigurationDetails>> _cache = new();
|
|
||||||
|
|
||||||
public IntegrationConfigurationDetailsCacheService(
|
|
||||||
IOrganizationIntegrationConfigurationRepository repository,
|
|
||||||
GlobalSettings globalSettings,
|
|
||||||
ILogger<IntegrationConfigurationDetailsCacheService> logger)
|
|
||||||
{
|
|
||||||
_repository = repository;
|
|
||||||
_logger = logger;
|
|
||||||
_refreshInterval = TimeSpan.FromMinutes(globalSettings.EventLogging.IntegrationCacheRefreshIntervalMinutes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<OrganizationIntegrationConfigurationDetails> GetConfigurationDetails(
|
|
||||||
Guid organizationId,
|
|
||||||
IntegrationType integrationType,
|
|
||||||
EventType eventType)
|
|
||||||
{
|
|
||||||
var specificKey = new IntegrationCacheKey(organizationId, integrationType, eventType);
|
|
||||||
var allEventsKey = new IntegrationCacheKey(organizationId, integrationType, null);
|
|
||||||
|
|
||||||
var results = new List<OrganizationIntegrationConfigurationDetails>();
|
|
||||||
|
|
||||||
if (_cache.TryGetValue(specificKey, out var specificConfigs))
|
|
||||||
{
|
|
||||||
results.AddRange(specificConfigs);
|
|
||||||
}
|
|
||||||
if (_cache.TryGetValue(allEventsKey, out var fallbackConfigs))
|
|
||||||
{
|
|
||||||
results.AddRange(fallbackConfigs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
||||||
{
|
|
||||||
await RefreshAsync();
|
|
||||||
|
|
||||||
var timer = new PeriodicTimer(_refreshInterval);
|
|
||||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
|
||||||
{
|
|
||||||
await RefreshAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task RefreshAsync()
|
|
||||||
{
|
|
||||||
var stopwatch = Stopwatch.StartNew();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var newCache = (await _repository.GetAllConfigurationDetailsAsync())
|
|
||||||
.GroupBy(x => new IntegrationCacheKey(x.OrganizationId, x.IntegrationType, x.EventType))
|
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
|
||||||
_cache = newCache;
|
|
||||||
|
|
||||||
stopwatch.Stop();
|
|
||||||
_logger.LogInformation(
|
|
||||||
"[IntegrationConfigurationDetailsCacheService] Refreshed successfully: {Count} entries in {Duration}ms",
|
|
||||||
newCache.Count,
|
|
||||||
stopwatch.Elapsed.TotalMilliseconds);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("[IntegrationConfigurationDetailsCacheService] Refresh failed: {ex}", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -295,33 +295,60 @@ graph TD
|
|||||||
```
|
```
|
||||||
## Caching
|
## Caching
|
||||||
|
|
||||||
To reduce database load and improve performance, integration configurations are cached in-memory as a Dictionary
|
To reduce database load and improve performance, event integrations uses its own named extended cache (see
|
||||||
with a periodic load of all configurations. Without caching, each incoming `EventMessage` would trigger a database
|
[CACHING in Utilities](https://github.com/bitwarden/server/blob/main/src/Core/Utilities/CACHING.md)
|
||||||
|
for more information). Without caching, for instance, each incoming `EventMessage` would trigger a database
|
||||||
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
|
query to retrieve the relevant `OrganizationIntegrationConfigurationDetails`.
|
||||||
|
|
||||||
By loading all configurations into memory on a fixed interval, we ensure:
|
### `EventIntegrationsCacheConstants`
|
||||||
|
|
||||||
- Consistent performance for reads.
|
`EventIntegrationsCacheConstants` allows the code to have strongly typed references to a number of cache-related
|
||||||
- Reduced database pressure.
|
details when working with the extended cache. The cache name and all cache keys and tags are programmatically accessed
|
||||||
- Predictable refresh timing, independent of event activity.
|
from `EventIntegrationsCacheConstants` rather than simple strings. For instance,
|
||||||
|
`EventIntegrationsCacheConstants.CacheName` is used in the cache setup, keyed services, dependency injection, etc.,
|
||||||
|
rather than using a string literal (i.e. "EventIntegrations") in code.
|
||||||
|
|
||||||
### Architecture / Design
|
### `OrganizationIntegrationConfigurationDetails`
|
||||||
|
|
||||||
- The cache is read-only for consumers. It is only updated in bulk by a background refresh process.
|
- This is one of the most actively used portions of the architecture because any event that has an associated
|
||||||
- The cache is fully replaced on each refresh to avoid locking or partial state.
|
organization requires a check of the configurations to determine if we need to fire off an integration.
|
||||||
|
- By using the extended cache, all reads are hitting the L1 or L2 cache before needing to access the database.
|
||||||
- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no
|
- Reads return a `List<OrganizationIntegrationConfigurationDetails>` for a given key or an empty list if no
|
||||||
match exists.
|
match exists.
|
||||||
- Failures or delays in the loading process do not affect the existing cache state. The cache will continue serving
|
- The TTL is set very high on these records (1 day). This is because when the admin API makes any changes, it
|
||||||
the last known good state until the update replaces the whole cache.
|
tells the cache to remove that key. This propagates to the event listening code via the extended cache backplane,
|
||||||
|
which means that the cache is then expired and the next read will fetch the new values. This allows us to have
|
||||||
|
a high TTL and avoid needing to refresh values except when necessary.
|
||||||
|
|
||||||
### Background Refresh
|
#### Tagging per integration
|
||||||
|
|
||||||
A hosted service (`IntegrationConfigurationDetailsCacheService`) runs in the background and:
|
- Each entry in the cache (which again, returns `List<OrganizationIntegrationConfigurationDetails>`) is tagged with
|
||||||
|
the organization id and the integration type.
|
||||||
|
- This allows us to remove all of a given organization's configuration details for an integration when the admin
|
||||||
|
makes changes at the integration level.
|
||||||
|
- For instance, if there were 5 events configured for a given organization's webhook and the admin changed the URL
|
||||||
|
at the integration level, the updates would need to be propagated or else the cache will continue returning the
|
||||||
|
stale URL.
|
||||||
|
- By tagging each of the entries, the API can ask the extended cache to remove all the entries for a given
|
||||||
|
organization integration in one call. The cache will handle dropping / refreshing these entries in a
|
||||||
|
performant way.
|
||||||
|
- There are two places in the code that are both aware of the tagging functionality
|
||||||
|
- The `EventIntegrationHandler` must use the tag when fetching relevant configuration details. This tells the cache
|
||||||
|
to store the entry with the tag when it successfully loads from the repository.
|
||||||
|
- The `CreateOrganizationIntegrationCommand`, `UpdateOrganizationIntegrationCommand`, and
|
||||||
|
`DeleteOrganizationIntegrationCommand` commands need to use the tag to remove all the tagged entries when an admin
|
||||||
|
creates, updates, or deletes an integration.
|
||||||
|
- To ensure both places are synchronized on how to tag entries, they both use
|
||||||
|
`EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration` to build the tag.
|
||||||
|
|
||||||
- Loads all configuration records at application startup.
|
### Template Properties
|
||||||
- Refreshes the cache on a configurable interval.
|
|
||||||
- Logs timing and entry count on success.
|
- The `IntegrationTemplateProcessor` supports some properties that require an additional lookup. For instance,
|
||||||
- Logs exceptions on failure without disrupting application flow.
|
the `UserId` is provided as part of the `EventMessage`, but `UserName` means an additional lookup to map the user
|
||||||
|
id to the actual name.
|
||||||
|
- The properties for a `User` (which includes `ActingUser`), `Group`, and `Organization` are cached via the
|
||||||
|
extended cache with a default TTL of 30 minutes.
|
||||||
|
- This is cached in both the L1 (Memory) and L2 (Redis) and will be automatically refreshed as needed.
|
||||||
|
|
||||||
# Building a new integration
|
# Building a new integration
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
@ -455,9 +456,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
|||||||
else if (!string.IsNullOrEmpty(organization.DisplayName()))
|
else if (!string.IsNullOrEmpty(organization.DisplayName()))
|
||||||
{
|
{
|
||||||
// If the organization is Free or Families plan, send families welcome email
|
// If the organization is Free or Families plan, send families welcome email
|
||||||
if (organization.PlanType is PlanType.FamiliesAnnually
|
if (organization.PlanType.GetProductTier() is ProductTierType.Free or ProductTierType.Families)
|
||||||
or PlanType.FamiliesAnnually2019
|
|
||||||
or PlanType.Free)
|
|
||||||
{
|
{
|
||||||
await _mailService.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.DisplayName());
|
await _mailService.SendFreeOrgOrFamilyOrgUserWelcomeEmailAsync(user, organization.DisplayName());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,6 +142,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||||
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
||||||
|
public const string BulkRevokeUsersV2 = "pm-28456-bulk-revoke-users-v2";
|
||||||
|
|
||||||
/* Architecture */
|
/* Architecture */
|
||||||
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
|
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
|
||||||
@ -242,15 +243,14 @@ public static class FeatureFlagKeys
|
|||||||
/* Vault Team */
|
/* Vault Team */
|
||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
||||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
|
||||||
public const string PhishingDetection = "phishing-detection";
|
public const string PhishingDetection = "phishing-detection";
|
||||||
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
|
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
|
||||||
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
public const string PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view";
|
||||||
public const string PM19315EndUserActivationMvp = "pm-19315-end-user-activation-mvp";
|
|
||||||
public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption";
|
public const string PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption";
|
||||||
public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium";
|
public const string PM23904_RiskInsightsForPremium = "pm-23904-risk-insights-for-premium";
|
||||||
public const string PM25083_AutofillConfirmFromSearch = "pm-25083-autofill-confirm-from-search";
|
public const string PM25083_AutofillConfirmFromSearch = "pm-25083-autofill-confirm-from-search";
|
||||||
public const string VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders";
|
public const string VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders";
|
||||||
|
public const string BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight";
|
||||||
|
|
||||||
/* Innovation Team */
|
/* Innovation Team */
|
||||||
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
|
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
|
||||||
|
|||||||
@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="4.0.1.3" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="4.0.2.5" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="4.0.1.5" />
|
<PackageReference Include="AWSSDK.SQS" Version="4.0.2.5" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.11.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.11.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
|
|||||||
@ -53,11 +53,37 @@
|
|||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-hero-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-learn-more-footer-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width:479px) {
|
@media only screen and (max-width:479px) {
|
||||||
table.mj-full-width-mobile { width: 100% !important; }
|
table.mj-full-width-mobile { width: 100% !important; }
|
||||||
td.mj-full-width-mobile { width: auto !important; }
|
td.mj-full-width-mobile { width: auto !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-icon-row-text {
|
||||||
|
padding-left: 5px !important;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.mj-bw-icon-row {
|
||||||
|
padding: 10px 15px;
|
||||||
|
width: fit-content !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@ -67,29 +93,8 @@
|
|||||||
.border-fix > table > tbody > tr > td {
|
.border-fix > table > tbody > tr > td {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.hide-small-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.send-bubble {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.mj-bw-icon-row-text {
|
|
||||||
padding-left: 5px !important;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.mj-bw-icon-row {
|
|
||||||
padding: 10px 15px;
|
|
||||||
width: fit-content !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<!-- Responsive icon visibility -->
|
|
||||||
<!-- Responsive styling for mj-bw-icon-row -->
|
|
||||||
</head>
|
</head>
|
||||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||||
|
|
||||||
@ -156,7 +161,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||||
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
||||||
Let's get set up to autofill.
|
Let’s get you set up to autofill.
|
||||||
</h2>
|
</h2>
|
||||||
</mj-text></div>
|
</mj-text></div>
|
||||||
|
|
||||||
@ -176,7 +181,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -256,7 +261,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">A <b>{{OrganizationName}}</b> administrator will approve you
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">An administrator from <b>{{OrganizationName}}</b> will approve you
|
||||||
before you can share passwords. While you wait for approval, get
|
before you can share passwords. While you wait for approval, get
|
||||||
started with Bitwarden Password Manager:</div>
|
started with Bitwarden Password Manager:</div>
|
||||||
|
|
||||||
@ -643,7 +648,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -53,11 +53,37 @@
|
|||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-hero-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-learn-more-footer-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width:479px) {
|
@media only screen and (max-width:479px) {
|
||||||
table.mj-full-width-mobile { width: 100% !important; }
|
table.mj-full-width-mobile { width: 100% !important; }
|
||||||
td.mj-full-width-mobile { width: auto !important; }
|
td.mj-full-width-mobile { width: auto !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-icon-row-text {
|
||||||
|
padding-left: 5px !important;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.mj-bw-icon-row {
|
||||||
|
padding: 10px 15px;
|
||||||
|
width: fit-content !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@ -67,29 +93,8 @@
|
|||||||
.border-fix > table > tbody > tr > td {
|
.border-fix > table > tbody > tr > td {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.hide-small-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.send-bubble {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.mj-bw-icon-row-text {
|
|
||||||
padding-left: 5px !important;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.mj-bw-icon-row {
|
|
||||||
padding: 10px 15px;
|
|
||||||
width: fit-content !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<!-- Responsive icon visibility -->
|
|
||||||
<!-- Responsive styling for mj-bw-icon-row -->
|
|
||||||
</head>
|
</head>
|
||||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||||
|
|
||||||
@ -156,7 +161,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||||
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
||||||
Let's get set up to autofill.
|
Let’s get you set up to autofill.
|
||||||
</h2>
|
</h2>
|
||||||
</mj-text></div>
|
</mj-text></div>
|
||||||
|
|
||||||
@ -176,7 +181,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -642,7 +647,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -53,11 +53,37 @@
|
|||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-hero-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-learn-more-footer-responsive-img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media only screen and (max-width:479px) {
|
@media only screen and (max-width:479px) {
|
||||||
table.mj-full-width-mobile { width: 100% !important; }
|
table.mj-full-width-mobile { width: 100% !important; }
|
||||||
td.mj-full-width-mobile { width: auto !important; }
|
td.mj-full-width-mobile { width: auto !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-icon-row-text {
|
||||||
|
padding-left: 5px !important;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.mj-bw-icon-row {
|
||||||
|
padding: 10px 15px;
|
||||||
|
width: fit-content !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
@ -67,29 +93,8 @@
|
|||||||
.border-fix > table > tbody > tr > td {
|
.border-fix > table > tbody > tr > td {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.hide-small-img {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.send-bubble {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
width: 90% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.mj-bw-icon-row-text {
|
|
||||||
padding-left: 5px !important;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
.mj-bw-icon-row {
|
|
||||||
padding: 10px 15px;
|
|
||||||
width: fit-content !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<!-- Responsive icon visibility -->
|
|
||||||
<!-- Responsive styling for mj-bw-icon-row -->
|
|
||||||
</head>
|
</head>
|
||||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||||
|
|
||||||
@ -156,7 +161,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
<mj-text color="#fff" padding-top="0" padding-bottom="0">
|
||||||
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
<h2 style="font-weight: normal; font-size: 16px; line-height: 0px">
|
||||||
Let's get set up to autofill.
|
Let’s get you set up to autofill.
|
||||||
</h2>
|
</h2>
|
||||||
</mj-text></div>
|
</mj-text></div>
|
||||||
|
|
||||||
@ -176,7 +181,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -256,7 +261,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
||||||
|
|
||||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">A <b>{{OrganizationName}}</b> administrator will need to confirm
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">An administrator from <b>{{OrganizationName}}</b> will need to confirm
|
||||||
you before you can share passwords. Get started with Bitwarden
|
you before you can share passwords. Get started with Bitwarden
|
||||||
Password Manager:</div>
|
Password Manager:</div>
|
||||||
|
|
||||||
@ -643,7 +648,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" class="hide-small-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="center" class="mj-bw-learn-more-footer-responsive-img" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -18,16 +18,16 @@ class MjBwIconRow extends BodyComponent {
|
|||||||
|
|
||||||
static defaultAttributes = {};
|
static defaultAttributes = {};
|
||||||
|
|
||||||
componentHeadStyle = (breakpoint) => {
|
headStyle = (breakpoint) => {
|
||||||
return `
|
return `
|
||||||
@media only screen and (max-width:${breakpoint}): {
|
@media only screen and (max-width:${breakpoint}) {
|
||||||
".mj-bw-icon-row-text": {
|
.mj-bw-icon-row-text {
|
||||||
padding-left: "5px !important",
|
padding-left: 5px !important;
|
||||||
line-height: "20px",
|
line-height: 20px;
|
||||||
},
|
}
|
||||||
".mj-bw-icon-row": {
|
.mj-bw-icon-row {
|
||||||
padding: "10px 15px",
|
padding: 10px 15px;
|
||||||
width: "fit-content !important",
|
width: fit-content !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<mj-bw-hero
|
<mj-bw-hero
|
||||||
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
||||||
title="Welcome to Bitwarden!"
|
title="Welcome to Bitwarden!"
|
||||||
sub-title="Let's get set up to autofill."
|
sub-title="Let’s get you set up to autofill."
|
||||||
/>
|
/>
|
||||||
</mj-wrapper>
|
</mj-wrapper>
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<mj-bw-hero
|
<mj-bw-hero
|
||||||
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
||||||
title="Welcome to Bitwarden!"
|
title="Welcome to Bitwarden!"
|
||||||
sub-title="Let's get set up to autofill."
|
sub-title="Let’s get you set up to autofill."
|
||||||
/>
|
/>
|
||||||
</mj-wrapper>
|
</mj-wrapper>
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
<mj-bw-hero
|
<mj-bw-hero
|
||||||
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
img-src="https://assets.bitwarden.com/email/v1/account-fill.png"
|
||||||
title="Welcome to Bitwarden!"
|
title="Welcome to Bitwarden!"
|
||||||
sub-title="Let's get set up to autofill."
|
sub-title="Let’s get you set up to autofill."
|
||||||
/>
|
/>
|
||||||
</mj-wrapper>
|
</mj-wrapper>
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,9 @@ using Microsoft.AspNetCore.DataProtection;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
using V1_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
|
using V2_RevokeUsersCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
namespace Bit.Core.OrganizationFeatures;
|
namespace Bit.Core.OrganizationFeatures;
|
||||||
|
|
||||||
public static class OrganizationServiceCollectionExtensions
|
public static class OrganizationServiceCollectionExtensions
|
||||||
@ -133,7 +136,6 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
services.AddScoped<IRemoveOrganizationUserCommand, RemoveOrganizationUserCommand>();
|
||||||
services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();
|
services.AddScoped<IRevokeNonCompliantOrganizationUserCommand, RevokeNonCompliantOrganizationUserCommand>();
|
||||||
services.AddScoped<IRevokeOrganizationUserCommand, RevokeOrganizationUserCommand>();
|
|
||||||
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
services.AddScoped<IUpdateOrganizationUserCommand, UpdateOrganizationUserCommand>();
|
||||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||||
@ -143,6 +145,11 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
|
|
||||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
||||||
|
|
||||||
|
services.AddScoped<V1_RevokeUsersCommand.IRevokeOrganizationUserCommand, V1_RevokeUsersCommand.RevokeOrganizationUserCommand>();
|
||||||
|
|
||||||
|
services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserCommand, V2_RevokeUsersCommand.RevokeOrganizationUserCommand>();
|
||||||
|
services.AddScoped<V2_RevokeUsersCommand.IRevokeOrganizationUserValidator, V2_RevokeUsersCommand.RevokeOrganizationUsersValidator>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services)
|
||||||
@ -197,6 +204,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();
|
services.AddScoped<IInviteOrganizationUsersCommand, InviteOrganizationUsersCommand>();
|
||||||
services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();
|
services.AddScoped<ISendOrganizationInvitesCommand, SendOrganizationInvitesCommand>();
|
||||||
services.AddScoped<IResendOrganizationInviteCommand, ResendOrganizationInviteCommand>();
|
services.AddScoped<IResendOrganizationInviteCommand, ResendOrganizationInviteCommand>();
|
||||||
|
services.AddScoped<IBulkResendOrganizationInvitesCommand, BulkResendOrganizationInvitesCommand>();
|
||||||
|
|
||||||
services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();
|
services.AddScoped<IInviteUsersValidator, InviteOrganizationUsersValidator>();
|
||||||
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
|
services.AddScoped<IInviteUsersOrganizationValidator, InviteUsersOrganizationValidator>();
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
namespace Bit.Core.Utilities;
|
namespace Bit.Core.Utilities;
|
||||||
@ -11,7 +13,12 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base cache name used for storing event integration data.
|
/// The base cache name used for storing event integration data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string CacheName = "EventIntegrations";
|
public const string CacheName = "EventIntegrations";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Duration TimeSpan for adding OrganizationIntegrationConfigurationDetails to the cache.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly TimeSpan DurationForOrganizationIntegrationConfigurationDetails = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for a <see cref="Group"/>.
|
/// Builds a deterministic cache key for a <see cref="Group"/>.
|
||||||
@ -20,10 +27,8 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for this Group.
|
/// A cache key for this Group.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForGroup(Guid groupId)
|
public static string BuildCacheKeyForGroup(Guid groupId) =>
|
||||||
{
|
$"Group:{groupId:N}";
|
||||||
return $"Group:{groupId:N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for an <see cref="Organization"/>.
|
/// Builds a deterministic cache key for an <see cref="Organization"/>.
|
||||||
@ -32,10 +37,8 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for the Organization.
|
/// A cache key for the Organization.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForOrganization(Guid organizationId)
|
public static string BuildCacheKeyForOrganization(Guid organizationId) =>
|
||||||
{
|
$"Organization:{organizationId:N}";
|
||||||
return $"Organization:{organizationId:N}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds a deterministic cache key for an organization user <see cref="OrganizationUserUserDetails"/>.
|
/// Builds a deterministic cache key for an organization user <see cref="OrganizationUserUserDetails"/>.
|
||||||
@ -45,8 +48,37 @@ public static class EventIntegrationsCacheConstants
|
|||||||
/// <returns>
|
/// <returns>
|
||||||
/// A cache key for the user.
|
/// A cache key for the user.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId)
|
public static string BuildCacheKeyForOrganizationUser(Guid organizationId, Guid userId) =>
|
||||||
{
|
$"OrganizationUserUserDetails:{organizationId:N}:{userId:N}";
|
||||||
return $"OrganizationUserUserDetails:{organizationId:N}:{userId:N}";
|
|
||||||
}
|
/// <summary>
|
||||||
|
/// Builds a deterministic cache key for an organization's integration configuration details
|
||||||
|
/// <see cref="OrganizationIntegrationConfigurationDetails"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param>
|
||||||
|
/// <param name="integrationType">The <see cref="IntegrationType"/> of the integration.</param>
|
||||||
|
/// <param name="eventType">The <see cref="EventType"/> of the event configured. Can be null to apply to all events.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cache key for the configuration details.
|
||||||
|
/// </returns>
|
||||||
|
public static string BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid organizationId,
|
||||||
|
IntegrationType integrationType,
|
||||||
|
EventType? eventType
|
||||||
|
) => $"OrganizationIntegrationConfigurationDetails:{organizationId:N}:{integrationType}:{eventType}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a deterministic tag for tagging an organization's integration configuration details. This tag is then
|
||||||
|
/// used to tag all of the <see cref="OrganizationIntegrationConfigurationDetails"/> that result from this
|
||||||
|
/// integration, which allows us to remove all relevant entries when an integration is changed or removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">The unique identifier of the organization to which the user belongs.</param>
|
||||||
|
/// <param name="integrationType">The <see cref="IntegrationType"/> of the integration.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A cache tag to use for the configuration details.
|
||||||
|
/// </returns>
|
||||||
|
public static string BuildCacheTagForOrganizationIntegration(
|
||||||
|
Guid organizationId,
|
||||||
|
IntegrationType integrationType
|
||||||
|
) => $"OrganizationIntegration:{organizationId:N}:{integrationType}";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,9 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Organiz
|
|||||||
: base(connectionString, readOnlyConnectionString)
|
: base(connectionString, readOnlyConnectionString)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
public async Task<List<OrganizationIntegrationConfigurationDetails>>
|
||||||
Guid organizationId,
|
GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType)
|
||||||
EventType eventType)
|
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -625,7 +625,11 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
|||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
|
"[dbo].[OrganizationUser_SetStatusForUsersByGuidIdArray]",
|
||||||
new { OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(), Status = OrganizationUserStatusType.Revoked },
|
new
|
||||||
|
{
|
||||||
|
OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP(),
|
||||||
|
Status = OrganizationUserStatusType.Revoked
|
||||||
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,16 +17,17 @@ public class OrganizationIntegrationConfigurationRepository : Repository<Core.Ad
|
|||||||
: base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations)
|
: base(serviceScopeFactory, mapper, context => context.OrganizationIntegrationConfigurations)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public async Task<List<OrganizationIntegrationConfigurationDetails>> GetConfigurationDetailsAsync(
|
public async Task<List<OrganizationIntegrationConfigurationDetails>>
|
||||||
Guid organizationId,
|
GetManyByEventTypeOrganizationIdIntegrationType(EventType eventType, Guid organizationId,
|
||||||
IntegrationType integrationType,
|
IntegrationType integrationType)
|
||||||
EventType eventType)
|
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
var query = new OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
||||||
organizationId, eventType, integrationType
|
organizationId,
|
||||||
|
eventType,
|
||||||
|
integrationType
|
||||||
);
|
);
|
||||||
return await query.Run(dbContext).ToListAsync();
|
return await query.Run(dbContext).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,21 @@
|
|||||||
#nullable enable
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
|
|
||||||
public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery : IQuery<OrganizationIntegrationConfigurationDetails>
|
public class OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(
|
||||||
|
Guid organizationId,
|
||||||
|
EventType eventType,
|
||||||
|
IntegrationType integrationType)
|
||||||
|
: IQuery<OrganizationIntegrationConfigurationDetails>
|
||||||
{
|
{
|
||||||
private readonly Guid _organizationId;
|
|
||||||
private readonly EventType _eventType;
|
|
||||||
private readonly IntegrationType _integrationType;
|
|
||||||
|
|
||||||
public OrganizationIntegrationConfigurationDetailsReadManyByEventTypeOrganizationIdIntegrationTypeQuery(Guid organizationId, EventType eventType, IntegrationType integrationType)
|
|
||||||
{
|
|
||||||
_organizationId = organizationId;
|
|
||||||
_eventType = eventType;
|
|
||||||
_integrationType = integrationType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)
|
public IQueryable<OrganizationIntegrationConfigurationDetails> Run(DatabaseContext dbContext)
|
||||||
{
|
{
|
||||||
var query = from oic in dbContext.OrganizationIntegrationConfigurations
|
var query = from oic in dbContext.OrganizationIntegrationConfigurations
|
||||||
join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id into oioic
|
join oi in dbContext.OrganizationIntegrations on oic.OrganizationIntegrationId equals oi.Id
|
||||||
from oi in dbContext.OrganizationIntegrations
|
where oi.OrganizationId == organizationId &&
|
||||||
where oi.OrganizationId == _organizationId &&
|
oi.Type == integrationType &&
|
||||||
oi.Type == _integrationType &&
|
(oic.EventType == eventType || oic.EventType == null)
|
||||||
oic.EventType == _eventType
|
|
||||||
select new OrganizationIntegrationConfigurationDetails()
|
select new OrganizationIntegrationConfigurationDetails()
|
||||||
{
|
{
|
||||||
Id = oic.Id,
|
Id = oic.Id,
|
||||||
|
|||||||
@ -893,13 +893,11 @@ public static class ServiceCollectionExtensions
|
|||||||
integrationType: listenerConfiguration.IntegrationType,
|
integrationType: listenerConfiguration.IntegrationType,
|
||||||
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
|
|
||||||
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
||||||
|
configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
||||||
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
|
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())
|
||||||
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
||||||
AzureServiceBusEventListenerService<TListenerConfig>>(provider =>
|
AzureServiceBusEventListenerService<TListenerConfig>>(provider =>
|
||||||
@ -941,10 +939,6 @@ public static class ServiceCollectionExtensions
|
|||||||
// Add common services
|
// Add common services
|
||||||
services.AddDistributedCache(globalSettings);
|
services.AddDistributedCache(globalSettings);
|
||||||
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
services.AddExtendedCache(EventIntegrationsCacheConstants.CacheName, globalSettings);
|
||||||
services.TryAddSingleton<IntegrationConfigurationDetailsCacheService>();
|
|
||||||
services.TryAddSingleton<IIntegrationConfigurationDetailsCache>(provider =>
|
|
||||||
provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
|
|
||||||
services.AddHostedService(provider => provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
|
|
||||||
services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
|
||||||
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||||
|
|
||||||
@ -1024,13 +1018,11 @@ public static class ServiceCollectionExtensions
|
|||||||
integrationType: listenerConfiguration.IntegrationType,
|
integrationType: listenerConfiguration.IntegrationType,
|
||||||
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
|
||||||
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
|
||||||
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
|
|
||||||
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
cache: provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName),
|
||||||
|
configurationRepository: provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||||
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
groupRepository: provider.GetRequiredService<IGroupRepository>(),
|
||||||
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
|
||||||
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(),
|
organizationUserRepository: provider.GetRequiredService<IOrganizationUserRepository>(), logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>())
|
||||||
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
|
||||||
RabbitMqEventListenerService<TListenerConfig>>(provider =>
|
RabbitMqEventListenerService<TListenerConfig>>(provider =>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ BEGIN
|
|||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
WHERE
|
WHERE
|
||||||
oic.[EventType] = @EventType
|
(oic.[EventType] = @EventType OR oic.[EventType] IS NULL)
|
||||||
AND
|
AND
|
||||||
oic.[OrganizationId] = @OrganizationId
|
oic.[OrganizationId] = @OrganizationId
|
||||||
AND
|
AND
|
||||||
|
|||||||
@ -0,0 +1,347 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||||
|
|
||||||
|
public class OrganizationUserControllerBulkRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
private readonly LoginHelper _loginHelper;
|
||||||
|
|
||||||
|
private Organization _organization = null!;
|
||||||
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
|
public OrganizationUserControllerBulkRevokeTests(ApiApplicationFactory apiFactory)
|
||||||
|
{
|
||||||
|
_factory = apiFactory;
|
||||||
|
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||||
|
{
|
||||||
|
featureService
|
||||||
|
.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2)
|
||||||
|
.Returns(true);
|
||||||
|
});
|
||||||
|
_client = _factory.CreateClient();
|
||||||
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_ownerEmail = $"org-user-bulk-revoke-test-{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
|
||||||
|
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseMonthly,
|
||||||
|
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_Success()
|
||||||
|
{
|
||||||
|
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(ownerEmail);
|
||||||
|
|
||||||
|
var (_, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
var (_, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [orgUser1.Id, orgUser2.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Equal(2, content.Data.Count());
|
||||||
|
Assert.All(content.Data, r => Assert.Empty(r.Error));
|
||||||
|
|
||||||
|
var actualUsers = await organizationUserRepository.GetManyAsync([orgUser1.Id, orgUser2.Id]);
|
||||||
|
Assert.All(actualUsers, u => Assert.Equal(OrganizationUserStatusType.Revoked, u.Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_AsAdmin_Success()
|
||||||
|
{
|
||||||
|
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Admin);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(adminEmail);
|
||||||
|
|
||||||
|
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [orgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Single(content.Data);
|
||||||
|
Assert.All(content.Data, r => Assert.Empty(r.Error));
|
||||||
|
|
||||||
|
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
|
||||||
|
Assert.NotNull(actualUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_CannotRevokeSelf_ReturnsError()
|
||||||
|
{
|
||||||
|
var (userEmail, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Admin);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(userEmail);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [orgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Single(content.Data);
|
||||||
|
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "You cannot revoke yourself.");
|
||||||
|
|
||||||
|
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
|
||||||
|
Assert.NotNull(actualUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_AlreadyRevoked_ReturnsError()
|
||||||
|
{
|
||||||
|
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(ownerEmail);
|
||||||
|
|
||||||
|
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
await organizationUserRepository.RevokeAsync(orgUser.Id);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [orgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Single(content.Data);
|
||||||
|
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "Already revoked.");
|
||||||
|
|
||||||
|
var actualUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
|
||||||
|
Assert.NotNull(actualUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_AdminCannotRevokeOwner_ReturnsError()
|
||||||
|
{
|
||||||
|
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Admin);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(adminEmail);
|
||||||
|
|
||||||
|
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [ownerOrgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Single(content.Data);
|
||||||
|
Assert.Contains(content.Data, r => r.Id == ownerOrgUser.Id && r.Error == "Only owners can revoke other owners.");
|
||||||
|
|
||||||
|
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(ownerOrgUser.Id);
|
||||||
|
Assert.NotNull(actualUser);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_MixedResults()
|
||||||
|
{
|
||||||
|
var (ownerEmail, requestingOwner) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(ownerEmail);
|
||||||
|
|
||||||
|
var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
var (_, alreadyRevokedOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
await organizationUserRepository.RevokeAsync(alreadyRevokedOrgUser.Id);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
Assert.NotNull(content);
|
||||||
|
Assert.Equal(3, content.Data.Count());
|
||||||
|
|
||||||
|
Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);
|
||||||
|
Assert.Contains(content.Data, r => r.Id == alreadyRevokedOrgUser.Id && r.Error == "Already revoked.");
|
||||||
|
Assert.Contains(content.Data, r => r.Id == requestingOwner.Id && r.Error == "You cannot revoke yourself.");
|
||||||
|
|
||||||
|
var actualUsers = await organizationUserRepository.GetManyAsync([validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == validOrgUser.Id).Status);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == alreadyRevokedOrgUser.Id).Status);
|
||||||
|
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUsers.First(u => u.Id == requestingOwner.Id).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(OrganizationUserType.User)]
|
||||||
|
[InlineData(OrganizationUserType.Custom)]
|
||||||
|
public async Task BulkRevoke_WithoutManageUsersPermission_ReturnsForbidden(OrganizationUserType organizationUserType)
|
||||||
|
{
|
||||||
|
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(userEmail);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [Guid.NewGuid()]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_WithEmptyIds_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
await _loginHelper.LoginAsync(_ownerEmail);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = []
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, httpResponse.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_WithInvalidOrganizationId_ReturnsForbidden()
|
||||||
|
{
|
||||||
|
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(ownerEmail);
|
||||||
|
|
||||||
|
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
|
||||||
|
|
||||||
|
var invalidOrgId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [orgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{invalidOrgId}/users/revoke", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BulkRevoke_ProviderRevokesOwner_ReturnsOk()
|
||||||
|
{
|
||||||
|
var providerEmail = $"provider-user{Guid.NewGuid()}@example.com";
|
||||||
|
|
||||||
|
// create user for provider
|
||||||
|
await _factory.LoginWithNewAccount(providerEmail);
|
||||||
|
|
||||||
|
// create provider and provider user
|
||||||
|
await _factory.GetService<ICreateProviderCommand>()
|
||||||
|
.CreateBusinessUnitAsync(
|
||||||
|
new Provider
|
||||||
|
{
|
||||||
|
Name = "provider",
|
||||||
|
Type = ProviderType.BusinessUnit
|
||||||
|
},
|
||||||
|
providerEmail,
|
||||||
|
PlanType.EnterpriseAnnually2023,
|
||||||
|
10);
|
||||||
|
|
||||||
|
await _loginHelper.LoginAsync(providerEmail);
|
||||||
|
|
||||||
|
var providerUserUser = await _factory.GetService<IUserRepository>().GetByEmailAsync(providerEmail);
|
||||||
|
|
||||||
|
var providerUserCollection = await _factory.GetService<IProviderUserRepository>()
|
||||||
|
.GetManyByUserAsync(providerUserUser!.Id);
|
||||||
|
|
||||||
|
var providerUser = providerUserCollection.First();
|
||||||
|
|
||||||
|
await _factory.GetService<IProviderOrganizationRepository>().CreateAsync(new ProviderOrganization
|
||||||
|
{
|
||||||
|
ProviderId = providerUser.ProviderId,
|
||||||
|
OrganizationId = _organization.Id,
|
||||||
|
Key = null,
|
||||||
|
Settings = null
|
||||||
|
});
|
||||||
|
|
||||||
|
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
|
||||||
|
_organization.Id, OrganizationUserType.Owner);
|
||||||
|
|
||||||
|
var request = new OrganizationUserBulkRequestModel
|
||||||
|
{
|
||||||
|
Ids = [ownerOrgUser.Id]
|
||||||
|
};
|
||||||
|
|
||||||
|
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,15 +2,14 @@
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||||
@ -19,7 +18,7 @@ namespace Bit.Api.Test.AdminConsole.Controllers;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class OrganizationIntegrationControllerTests
|
public class OrganizationIntegrationControllerTests
|
||||||
{
|
{
|
||||||
private OrganizationIntegrationRequestModel _webhookRequestModel = new OrganizationIntegrationRequestModel()
|
private readonly OrganizationIntegrationRequestModel _webhookRequestModel = new()
|
||||||
{
|
{
|
||||||
Configuration = null,
|
Configuration = null,
|
||||||
Type = IntegrationType.Webhook
|
Type = IntegrationType.Webhook
|
||||||
@ -48,13 +47,13 @@ public class OrganizationIntegrationControllerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>()
|
||||||
.GetManyByOrganizationAsync(organizationId)
|
.GetManyByOrganizationAsync(organizationId)
|
||||||
.Returns(integrations);
|
.Returns(integrations);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetAsync(organizationId);
|
var result = await sutProvider.Sut.GetAsync(organizationId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
await sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>().Received(1)
|
||||||
.GetManyByOrganizationAsync(organizationId);
|
.GetManyByOrganizationAsync(organizationId);
|
||||||
|
|
||||||
Assert.Equal(integrations.Count, result.Count);
|
Assert.Equal(integrations.Count, result.Count);
|
||||||
@ -70,7 +69,7 @@ public class OrganizationIntegrationControllerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
sutProvider.GetDependency<IGetOrganizationIntegrationsQuery>()
|
||||||
.GetManyByOrganizationAsync(organizationId)
|
.GetManyByOrganizationAsync(organizationId)
|
||||||
.Returns([]);
|
.Returns([]);
|
||||||
|
|
||||||
@ -80,199 +79,133 @@ public class OrganizationIntegrationControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task CreateAsync_Webhook_AllParamsProvided_Succeeds(
|
public async Task CreateAsync_AllParamsProvided_Succeeds(
|
||||||
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationIntegration integration)
|
||||||
|
{
|
||||||
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
|
.OrganizationOwner(organizationId)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()
|
||||||
|
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||||
|
.Returns(integration);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>().Received(1)
|
||||||
|
.CreateAsync(Arg.Is<OrganizationIntegration>(i =>
|
||||||
|
i.OrganizationId == organizationId &&
|
||||||
|
i.Type == IntegrationType.Webhook));
|
||||||
|
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId)
|
Guid organizationId)
|
||||||
{
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.OrganizationOwner(organizationId)
|
|
||||||
.Returns(true);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
|
||||||
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
|
|
||||||
var response = await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
|
||||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
|
||||||
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
|
||||||
Assert.Equal(IntegrationType.Webhook, response.Type);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationController> sutProvider, Guid organizationId)
|
|
||||||
{
|
{
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel));
|
await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||||
|
await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task DeleteAsync_AllParamsProvided_Succeeds(
|
public async Task DeleteAsync_AllParamsProvided_Succeeds(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
OrganizationIntegration organizationIntegration)
|
Guid integrationId)
|
||||||
{
|
{
|
||||||
organizationIntegration.OrganizationId = organizationId;
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.Returns(organizationIntegration);
|
|
||||||
|
|
||||||
await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id);
|
await sutProvider.Sut.DeleteAsync(organizationId, integrationId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
await sutProvider.GetDependency<IDeleteOrganizationIntegrationCommand>().Received(1)
|
||||||
.GetByIdAsync(organizationIntegration.Id);
|
.DeleteAsync(organizationId, integrationId);
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
|
||||||
.DeleteAsync(organizationIntegration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
[Obsolete("Obsolete")]
|
||||||
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
|
public async Task PostDeleteAsync_AllParamsProvided_Succeeds(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
OrganizationIntegration organizationIntegration)
|
Guid integrationId)
|
||||||
{
|
|
||||||
organizationIntegration.OrganizationId = organizationId;
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.OrganizationOwner(organizationId)
|
|
||||||
.Returns(true);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.Returns(organizationIntegration);
|
|
||||||
|
|
||||||
await sutProvider.Sut.PostDeleteAsync(organizationId, organizationIntegration.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
|
||||||
.GetByIdAsync(organizationIntegration.Id);
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
|
||||||
.DeleteAsync(organizationIntegration);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationIntegration organizationIntegration)
|
|
||||||
{
|
|
||||||
organizationIntegration.OrganizationId = Guid.NewGuid();
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.OrganizationOwner(organizationId)
|
|
||||||
.Returns(true);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
|
||||||
Guid organizationId)
|
|
||||||
{
|
{
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
await sutProvider.Sut.PostDeleteAsync(organizationId, integrationId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IDeleteOrganizationIntegrationCommand>().Received(1)
|
||||||
|
.DeleteAsync(organizationId, integrationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId)
|
Guid organizationId,
|
||||||
|
Guid integrationId)
|
||||||
{
|
{
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||||
|
await sutProvider.Sut.DeleteAsync(organizationId, integrationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateAsync_AllParamsProvided_Succeeds(
|
public async Task UpdateAsync_AllParamsProvided_Succeeds(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
OrganizationIntegration organizationIntegration)
|
Guid integrationId,
|
||||||
|
OrganizationIntegration integration)
|
||||||
{
|
{
|
||||||
organizationIntegration.OrganizationId = organizationId;
|
integration.OrganizationId = organizationId;
|
||||||
organizationIntegration.Type = IntegrationType.Webhook;
|
integration.Id = integrationId;
|
||||||
|
integration.Type = IntegrationType.Webhook;
|
||||||
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
sutProvider.GetDependency<IUpdateOrganizationIntegrationCommand>()
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
.UpdateAsync(organizationId, integrationId, Arg.Any<OrganizationIntegration>())
|
||||||
.Returns(organizationIntegration);
|
.Returns(integration);
|
||||||
|
|
||||||
var response = await sutProvider.Sut.UpdateAsync(organizationId, organizationIntegration.Id, _webhookRequestModel);
|
var response = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, _webhookRequestModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
await sutProvider.GetDependency<IUpdateOrganizationIntegrationCommand>().Received(1)
|
||||||
.GetByIdAsync(organizationIntegration.Id);
|
.UpdateAsync(organizationId, integrationId, Arg.Is<OrganizationIntegration>(i =>
|
||||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
i.OrganizationId == organizationId &&
|
||||||
.ReplaceAsync(organizationIntegration);
|
i.Type == IntegrationType.Webhook));
|
||||||
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
||||||
Assert.Equal(IntegrationType.Webhook, response.Type);
|
Assert.Equal(IntegrationType.Webhook, response.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
|
||||||
Guid organizationId,
|
|
||||||
OrganizationIntegration organizationIntegration)
|
|
||||||
{
|
|
||||||
organizationIntegration.OrganizationId = Guid.NewGuid();
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.OrganizationOwner(organizationId)
|
|
||||||
.Returns(true);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
|
||||||
Guid organizationId)
|
|
||||||
{
|
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
|
||||||
.OrganizationOwner(organizationId)
|
|
||||||
.Returns(true);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
|
||||||
.GetByIdAsync(Arg.Any<Guid>())
|
|
||||||
.ReturnsNull();
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||||
Guid organizationId)
|
Guid organizationId,
|
||||||
|
Guid integrationId)
|
||||||
{
|
{
|
||||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.OrganizationOwner(organizationId)
|
.OrganizationOwner(organizationId)
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
await Assert.ThrowsAsync<NotFoundException>(async () =>
|
||||||
|
await sutProvider.Sut.UpdateAsync(organizationId, integrationId, _webhookRequestModel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -730,4 +731,68 @@ public class OrganizationUsersControllerTests
|
|||||||
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
|
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
|
||||||
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
|
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkReinvite_WhenFeatureFlagEnabled_UsesBulkResendOrganizationInvitesCommand(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUserBulkRequestModel bulkRequestModel,
|
||||||
|
List<OrganizationUser> organizationUsers,
|
||||||
|
Guid userId,
|
||||||
|
SutProvider<OrganizationUsersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
|
||||||
|
sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
|
||||||
|
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
|
||||||
|
.Returns(expectedResults);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(organizationUsers.Count, response.Data.Count());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkReinvite_WhenFeatureFlagDisabled_UsesLegacyOrganizationService(
|
||||||
|
Guid organizationId,
|
||||||
|
OrganizationUserBulkRequestModel bulkRequestModel,
|
||||||
|
List<OrganizationUser> organizationUsers,
|
||||||
|
Guid userId,
|
||||||
|
SutProvider<OrganizationUsersController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
|
||||||
|
sutProvider.GetDependency<IOrganizationService>()
|
||||||
|
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
|
||||||
|
.Returns(expectedResults);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(organizationUsers.Count, response.Data.Count());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>()
|
||||||
|
.Received(1)
|
||||||
|
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,161 @@
|
|||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations.Interfaces;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using NSubstitute;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using Xunit;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.EventIntegrations;
|
||||||
|
|
||||||
|
public class EventIntegrationServiceCollectionExtensionsTests
|
||||||
|
{
|
||||||
|
private readonly IServiceCollection _services;
|
||||||
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
|
||||||
|
public EventIntegrationServiceCollectionExtensionsTests()
|
||||||
|
{
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_globalSettings = CreateGlobalSettings([]);
|
||||||
|
|
||||||
|
// Add required infrastructure services
|
||||||
|
_services.TryAddSingleton(_globalSettings);
|
||||||
|
_services.TryAddSingleton<IGlobalSettings>(_globalSettings);
|
||||||
|
_services.AddLogging();
|
||||||
|
|
||||||
|
// Mock Redis connection for cache
|
||||||
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>());
|
||||||
|
|
||||||
|
// Mock required repository dependencies for commands
|
||||||
|
_services.TryAddScoped(_ => Substitute.For<IOrganizationIntegrationRepository>());
|
||||||
|
_services.TryAddScoped(_ => Substitute.For<IOrganizationRepository>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEventIntegrationsCommandsQueries_RegistersAllServices()
|
||||||
|
{
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
|
||||||
|
using var provider = _services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(EventIntegrationsCacheConstants.CacheName);
|
||||||
|
Assert.NotNull(cache);
|
||||||
|
|
||||||
|
using var scope = provider.CreateScope();
|
||||||
|
var sp = scope.ServiceProvider;
|
||||||
|
|
||||||
|
Assert.NotNull(sp.GetService<ICreateOrganizationIntegrationCommand>());
|
||||||
|
Assert.NotNull(sp.GetService<IUpdateOrganizationIntegrationCommand>());
|
||||||
|
Assert.NotNull(sp.GetService<IDeleteOrganizationIntegrationCommand>());
|
||||||
|
Assert.NotNull(sp.GetService<IGetOrganizationIntegrationsQuery>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEventIntegrationsCommandsQueries_CommandsQueries_AreRegisteredAsScoped()
|
||||||
|
{
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
|
||||||
|
var createIntegrationDescriptor = _services.First(s =>
|
||||||
|
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
|
||||||
|
|
||||||
|
Assert.Equal(ServiceLifetime.Scoped, createIntegrationDescriptor.Lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEventIntegrationsCommandsQueries_CommandsQueries_DifferentInstancesPerScope()
|
||||||
|
{
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
|
||||||
|
var provider = _services.BuildServiceProvider();
|
||||||
|
|
||||||
|
ICreateOrganizationIntegrationCommand? instance1, instance2, instance3;
|
||||||
|
using (var scope1 = provider.CreateScope())
|
||||||
|
{
|
||||||
|
instance1 = scope1.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
|
||||||
|
}
|
||||||
|
using (var scope2 = provider.CreateScope())
|
||||||
|
{
|
||||||
|
instance2 = scope2.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
|
||||||
|
}
|
||||||
|
using (var scope3 = provider.CreateScope())
|
||||||
|
{
|
||||||
|
instance3 = scope3.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.NotNull(instance1);
|
||||||
|
Assert.NotNull(instance2);
|
||||||
|
Assert.NotNull(instance3);
|
||||||
|
Assert.NotSame(instance1, instance2);
|
||||||
|
Assert.NotSame(instance2, instance3);
|
||||||
|
Assert.NotSame(instance1, instance3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEventIntegrationsCommandsQueries_CommandsQueries__SameInstanceWithinScope()
|
||||||
|
{
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
var provider = _services.BuildServiceProvider();
|
||||||
|
|
||||||
|
using var scope = provider.CreateScope();
|
||||||
|
var instance1 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
|
||||||
|
var instance2 = scope.ServiceProvider.GetService<ICreateOrganizationIntegrationCommand>();
|
||||||
|
|
||||||
|
Assert.NotNull(instance1);
|
||||||
|
Assert.NotNull(instance2);
|
||||||
|
Assert.Same(instance1, instance2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddEventIntegrationsCommandsQueries_MultipleCalls_IsIdempotent()
|
||||||
|
{
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
_services.AddEventIntegrationsCommandsQueries(_globalSettings);
|
||||||
|
|
||||||
|
var createConfigCmdDescriptors = _services.Where(s =>
|
||||||
|
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList();
|
||||||
|
Assert.Single(createConfigCmdDescriptors);
|
||||||
|
|
||||||
|
var updateIntegrationCmdDescriptors = _services.Where(s =>
|
||||||
|
s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand)).ToList();
|
||||||
|
Assert.Single(updateIntegrationCmdDescriptors);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddOrganizationIntegrationCommandsQueries_RegistersAllIntegrationServices()
|
||||||
|
{
|
||||||
|
_services.AddOrganizationIntegrationCommandsQueries();
|
||||||
|
|
||||||
|
Assert.Contains(_services, s => s.ServiceType == typeof(ICreateOrganizationIntegrationCommand));
|
||||||
|
Assert.Contains(_services, s => s.ServiceType == typeof(IUpdateOrganizationIntegrationCommand));
|
||||||
|
Assert.Contains(_services, s => s.ServiceType == typeof(IDeleteOrganizationIntegrationCommand));
|
||||||
|
Assert.Contains(_services, s => s.ServiceType == typeof(IGetOrganizationIntegrationsQuery));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddOrganizationIntegrationCommandsQueries_MultipleCalls_IsIdempotent()
|
||||||
|
{
|
||||||
|
_services.AddOrganizationIntegrationCommandsQueries();
|
||||||
|
_services.AddOrganizationIntegrationCommandsQueries();
|
||||||
|
_services.AddOrganizationIntegrationCommandsQueries();
|
||||||
|
|
||||||
|
var createCmdDescriptors = _services.Where(s =>
|
||||||
|
s.ServiceType == typeof(ICreateOrganizationIntegrationCommand)).ToList();
|
||||||
|
Assert.Single(createCmdDescriptors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data)
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(data)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var settings = new GlobalSettings();
|
||||||
|
config.GetSection("GlobalSettings").Bind(settings);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class CreateOrganizationIntegrationCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAsync_Success_CreatesIntegrationAndInvalidatesCache(
|
||||||
|
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
OrganizationIntegration integration)
|
||||||
|
{
|
||||||
|
integration.Type = IntegrationType.Webhook;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||||
|
.Returns([]);
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.CreateAsync(integration)
|
||||||
|
.Returns(integration);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.CreateAsync(integration);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.GetManyByOrganizationAsync(integration.OrganizationId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.CreateAsync(integration);
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().Received(1)
|
||||||
|
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
integration.OrganizationId,
|
||||||
|
integration.Type));
|
||||||
|
Assert.Equal(integration, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAsync_DuplicateType_ThrowsBadRequest(
|
||||||
|
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
OrganizationIntegration integration,
|
||||||
|
OrganizationIntegration existingIntegration)
|
||||||
|
{
|
||||||
|
integration.Type = IntegrationType.Webhook;
|
||||||
|
existingIntegration.Type = IntegrationType.Webhook;
|
||||||
|
existingIntegration.OrganizationId = integration.OrganizationId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||||
|
.Returns([existingIntegration]);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.CreateAsync(integration));
|
||||||
|
|
||||||
|
Assert.Contains("An integration of this type already exists", exception.Message);
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAsync_DifferentType_Success(
|
||||||
|
SutProvider<CreateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
OrganizationIntegration integration,
|
||||||
|
OrganizationIntegration existingIntegration)
|
||||||
|
{
|
||||||
|
integration.Type = IntegrationType.Webhook;
|
||||||
|
existingIntegration.Type = IntegrationType.Slack;
|
||||||
|
existingIntegration.OrganizationId = integration.OrganizationId;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetManyByOrganizationAsync(integration.OrganizationId)
|
||||||
|
.Returns([existingIntegration]);
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.CreateAsync(integration)
|
||||||
|
.Returns(integration);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.CreateAsync(integration);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.CreateAsync(integration);
|
||||||
|
Assert.Equal(integration, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class DeleteOrganizationIntegrationCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DeleteAsync_Success_DeletesIntegrationAndInvalidatesCache(
|
||||||
|
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration integration)
|
||||||
|
{
|
||||||
|
integration.Id = integrationId;
|
||||||
|
integration.OrganizationId = organizationId;
|
||||||
|
integration.Type = IntegrationType.Webhook;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns(integration);
|
||||||
|
|
||||||
|
await sutProvider.Sut.DeleteAsync(organizationId, integrationId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.GetByIdAsync(integrationId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.DeleteAsync(integration);
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().Received(1)
|
||||||
|
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId,
|
||||||
|
integration.Type));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||||
|
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns((OrganizationIntegration)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.DeleteAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||||
|
SutProvider<DeleteOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration integration)
|
||||||
|
{
|
||||||
|
integration.Id = integrationId;
|
||||||
|
integration.OrganizationId = Guid.NewGuid(); // Different organization
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns(integration);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.DeleteAsync(organizationId, integrationId));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.DeleteAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class GetOrganizationIntegrationsQueryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetManyByOrganizationAsync_CallsRepository(
|
||||||
|
SutProvider<GetOrganizationIntegrationsQuery> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
List<OrganizationIntegration> integrations)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetManyByOrganizationAsync(organizationId)
|
||||||
|
.Returns(integrations);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.GetManyByOrganizationAsync(organizationId);
|
||||||
|
Assert.Equal(integrations.Count, result.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetManyByOrganizationAsync_NoIntegrations_ReturnsEmptyList(
|
||||||
|
SutProvider<GetOrganizationIntegrationsQuery> sutProvider,
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetManyByOrganizationAsync(organizationId)
|
||||||
|
.Returns([]);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.GetManyByOrganizationAsync(organizationId);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using ZiggyCreatures.Caching.Fusion;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.EventIntegrations.OrganizationIntegrations;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class UpdateOrganizationIntegrationCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_Success_UpdatesIntegrationAndInvalidatesCache(
|
||||||
|
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration existingIntegration,
|
||||||
|
OrganizationIntegration updatedIntegration)
|
||||||
|
{
|
||||||
|
existingIntegration.Id = integrationId;
|
||||||
|
existingIntegration.OrganizationId = organizationId;
|
||||||
|
existingIntegration.Type = IntegrationType.Webhook;
|
||||||
|
updatedIntegration.Id = integrationId;
|
||||||
|
updatedIntegration.OrganizationId = organizationId;
|
||||||
|
updatedIntegration.Type = IntegrationType.Webhook;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns(existingIntegration);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.GetByIdAsync(integrationId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||||
|
.ReplaceAsync(updatedIntegration);
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().Received(1)
|
||||||
|
.RemoveByTagAsync(EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
organizationId,
|
||||||
|
existingIntegration.Type));
|
||||||
|
Assert.Equal(updatedIntegration, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||||
|
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration updatedIntegration)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns((OrganizationIntegration)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||||
|
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration existingIntegration,
|
||||||
|
OrganizationIntegration updatedIntegration)
|
||||||
|
{
|
||||||
|
existingIntegration.Id = integrationId;
|
||||||
|
existingIntegration.OrganizationId = Guid.NewGuid(); // Different organization
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns(existingIntegration);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task UpdateAsync_IntegrationIsDifferentType_ThrowsNotFound(
|
||||||
|
SutProvider<UpdateOrganizationIntegrationCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid integrationId,
|
||||||
|
OrganizationIntegration existingIntegration,
|
||||||
|
OrganizationIntegration updatedIntegration)
|
||||||
|
{
|
||||||
|
existingIntegration.Id = integrationId;
|
||||||
|
existingIntegration.OrganizationId = organizationId;
|
||||||
|
existingIntegration.Type = IntegrationType.Webhook;
|
||||||
|
updatedIntegration.Id = integrationId;
|
||||||
|
updatedIntegration.OrganizationId = organizationId;
|
||||||
|
updatedIntegration.Type = IntegrationType.Hec; // Different Type
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||||
|
.GetByIdAsync(integrationId)
|
||||||
|
.Returns(existingIntegration);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(
|
||||||
|
() => sutProvider.Sut.UpdateAsync(organizationId, integrationId, updatedIntegration));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().DidNotReceive()
|
||||||
|
.ReplaceAsync(Arg.Any<OrganizationIntegration>());
|
||||||
|
await sutProvider.GetDependency<IFusionCache>().DidNotReceive()
|
||||||
|
.RemoveByTagAsync(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class BulkResendOrganizationInvitesCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkResendInvitesAsync_ValidatesUsersAndSendsBatchInvite(
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser validUser1,
|
||||||
|
OrganizationUser validUser2,
|
||||||
|
OrganizationUser acceptedUser,
|
||||||
|
OrganizationUser wrongOrgUser,
|
||||||
|
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
|
||||||
|
{
|
||||||
|
validUser1.OrganizationId = organization.Id;
|
||||||
|
validUser1.Status = OrganizationUserStatusType.Invited;
|
||||||
|
validUser2.OrganizationId = organization.Id;
|
||||||
|
validUser2.Status = OrganizationUserStatusType.Invited;
|
||||||
|
acceptedUser.OrganizationId = organization.Id;
|
||||||
|
acceptedUser.Status = OrganizationUserStatusType.Accepted;
|
||||||
|
wrongOrgUser.OrganizationId = Guid.NewGuid();
|
||||||
|
wrongOrgUser.Status = OrganizationUserStatusType.Invited;
|
||||||
|
|
||||||
|
var users = new List<OrganizationUser> { validUser1, validUser2, acceptedUser, wrongOrgUser };
|
||||||
|
var userIds = users.Select(u => u.Id).ToList();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(users);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();
|
||||||
|
|
||||||
|
Assert.Equal(4, result.Count);
|
||||||
|
Assert.Equal(2, result.Count(r => string.IsNullOrEmpty(r.Item2)));
|
||||||
|
Assert.Equal(2, result.Count(r => r.Item2 == "User invalid."));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.SendInvitesAsync(Arg.Is<SendInvitesRequest>(req =>
|
||||||
|
req.Organization == organization &&
|
||||||
|
req.Users.Length == 2 &&
|
||||||
|
req.InitOrganization == false));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkResendInvitesAsync_AllInvalidUsers_DoesNotSendInvites(
|
||||||
|
Organization organization,
|
||||||
|
List<OrganizationUser> organizationUsers,
|
||||||
|
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
|
||||||
|
{
|
||||||
|
foreach (var user in organizationUsers)
|
||||||
|
{
|
||||||
|
user.OrganizationId = organization.Id;
|
||||||
|
user.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIds = organizationUsers.Select(u => u.Id).ToList();
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
var result = (await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, userIds)).ToList();
|
||||||
|
|
||||||
|
Assert.Equal(organizationUsers.Count, result.Count);
|
||||||
|
Assert.All(result, r => Assert.Equal("User invalid.", r.Item2));
|
||||||
|
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()
|
||||||
|
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkResendInvitesAsync_OrganizationNotFound_ThrowsNotFoundException(
|
||||||
|
Guid organizationId,
|
||||||
|
List<Guid> userIds,
|
||||||
|
List<OrganizationUser> organizationUsers,
|
||||||
|
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(userIds).Returns(organizationUsers);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationId).Returns((Organization?)null);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
|
sutProvider.Sut.BulkResendInvitesAsync(organizationId, null, userIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task BulkResendInvitesAsync_EmptyUserList_ReturnsEmpty(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<BulkResendOrganizationInvitesCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var emptyUserIds = new List<Guid>();
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyAsync(emptyUserIds).Returns(new List<OrganizationUser>());
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.BulkResendInvitesAsync(organization.Id, null, emptyUserIds);
|
||||||
|
|
||||||
|
Assert.Empty(result);
|
||||||
|
await sutProvider.GetDependency<ISendOrganizationInvitesCommand>().DidNotReceive()
|
||||||
|
.SendInvitesAsync(Arg.Any<SendInvitesRequest>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
|||||||
@ -0,0 +1,215 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class RevokeOrganizationUserCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RevokeUsersAsync_WithValidUsers_RevokesUsersAndLogsEvents(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||||
|
orgUser1.UserId = Guid.NewGuid();
|
||||||
|
orgUser2.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = new RevokeOrganizationUsersRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser1.Id, orgUser2.Id],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);
|
||||||
|
SetupValidatorMock(sutProvider, [
|
||||||
|
ValidationResultHelpers.Valid(orgUser1),
|
||||||
|
ValidationResultHelpers.Valid(orgUser2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
Assert.All(results, r => Assert.True(r.Result.IsSuccess));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||||
|
ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, DateTime?)>>(
|
||||||
|
events => events.Count() == 2));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.Received(1)
|
||||||
|
.PushSyncOrgKeysAsync(orgUser1.UserId!.Value);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.Received(1)
|
||||||
|
.PushSyncOrgKeysAsync(orgUser2.UserId!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RevokeUsersAsync_WithSystemUser_LogsEventsWithSystemUserType(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.OrganizationId = organizationId;
|
||||||
|
orgUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);
|
||||||
|
|
||||||
|
var request = new RevokeOrganizationUsersRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser.Id],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
SetupRepositoryMocks(sutProvider, [orgUser]);
|
||||||
|
SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.RevokeUsersAsync(request);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventsAsync(Arg.Is<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>(
|
||||||
|
events => events.All(e => e.Item3 == EventSystemUser.SCIM)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RevokeUsersAsync_WithValidationErrors_ReturnsErrorResults(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
|
||||||
|
var request = new RevokeOrganizationUsersRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser1.Id, orgUser2.Id],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
SetupRepositoryMocks(sutProvider, [orgUser1, orgUser2]);
|
||||||
|
SetupValidatorMock(sutProvider, [
|
||||||
|
ValidationResultHelpers.Invalid(orgUser1, new UserAlreadyRevoked()),
|
||||||
|
ValidationResultHelpers.Valid(orgUser2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
var result1 = results.Single(r => r.Id == orgUser1.Id);
|
||||||
|
var result2 = results.Single(r => r.Id == orgUser2.Id);
|
||||||
|
|
||||||
|
Assert.True(result1.Result.IsError);
|
||||||
|
Assert.True(result2.Result.IsSuccess);
|
||||||
|
|
||||||
|
// Only the valid user should be revoked
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RevokeManyByIdAsync(Arg.Is<IEnumerable<Guid>>(ids =>
|
||||||
|
ids.Count() == 1 && ids.Contains(orgUser2.Id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task RevokeUsersAsync_WhenPushNotificationFails_ContinuesProcessing(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.OrganizationId = organizationId;
|
||||||
|
orgUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
|
||||||
|
var request = new RevokeOrganizationUsersRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser.Id],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
SetupRepositoryMocks(sutProvider, [orgUser]);
|
||||||
|
SetupValidatorMock(sutProvider, [ValidationResultHelpers.Valid(orgUser)]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.PushSyncOrgKeysAsync(orgUser.UserId!.Value)
|
||||||
|
.Returns(Task.FromException(new Exception("Push notification failed")));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.RevokeUsersAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results[0].Result.IsSuccess);
|
||||||
|
|
||||||
|
// Should log warning but continue
|
||||||
|
sutProvider.GetDependency<ILogger<RevokeOrganizationUserCommand>>()
|
||||||
|
.Received()
|
||||||
|
.Log(
|
||||||
|
LogLevel.Warning,
|
||||||
|
Arg.Any<EventId>(),
|
||||||
|
Arg.Any<object>(),
|
||||||
|
Arg.Any<Exception>(),
|
||||||
|
Arg.Any<Func<object, Exception?, string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>
|
||||||
|
(userId, systemUserType) switch
|
||||||
|
{
|
||||||
|
({ } id, _) => new StandardUser(id, isOwnerOrProvider),
|
||||||
|
(null, { } type) => new SystemUser(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void SetupRepositoryMocks(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
ICollection<OrganizationUser> organizationUsers)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(organizationUsers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetupValidatorMock(
|
||||||
|
SutProvider<RevokeOrganizationUserCommand> sutProvider,
|
||||||
|
ICollection<ValidationResult<OrganizationUser>> validationResults)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IRevokeOrganizationUserValidator>()
|
||||||
|
.ValidateAsync(Arg.Any<RevokeOrganizationUsersValidationRequest>())
|
||||||
|
.Returns(validationResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,325 @@
|
|||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class RevokeOrganizationUsersValidatorTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WithValidUsers_ReturnsSuccess(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = organizationId;
|
||||||
|
orgUser1.UserId = Guid.NewGuid();
|
||||||
|
orgUser2.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser1, orgUser2],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
Assert.All(results, r => Assert.True(r.IsValid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WithRevokedUser_ReturnsErrorForThatUser(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
revokedUser.OrganizationId = organizationId;
|
||||||
|
revokedUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[revokedUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsError);
|
||||||
|
Assert.IsType<UserAlreadyRevoked>(results.First().AsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WhenRevokingSelf_ReturnsErrorForThatUser(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.OrganizationId = organizationId;
|
||||||
|
orgUser.UserId = actingUserId;
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsError);
|
||||||
|
Assert.IsType<CannotRevokeYourself>(results.First().AsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WhenNonOwnerRevokesOwner_ReturnsErrorForThatUser(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ownerUser.OrganizationId = organizationId;
|
||||||
|
ownerUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[ownerUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsError);
|
||||||
|
Assert.IsType<OnlyOwnersCanRevokeOwners>(results.First().AsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WhenOwnerRevokesOwner_ReturnsSuccess(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
ownerUser.OrganizationId = organizationId;
|
||||||
|
ownerUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, true, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[ownerUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WithMultipleUsers_SomeValid_ReturnsMixedResults(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser validUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
validUser.OrganizationId = revokedUser.OrganizationId = organizationId;
|
||||||
|
validUser.UserId = Guid.NewGuid();
|
||||||
|
revokedUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[validUser, revokedUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
var validResult = results.Single(r => r.Request.Id == validUser.Id);
|
||||||
|
var errorResult = results.Single(r => r.Request.Id == revokedUser.Id);
|
||||||
|
|
||||||
|
Assert.True(validResult.IsValid);
|
||||||
|
Assert.True(errorResult.IsError);
|
||||||
|
Assert.IsType<UserAlreadyRevoked>(errorResult.AsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WithSystemUser_DoesNotRequireActingUserId(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
orgUser.OrganizationId = organizationId;
|
||||||
|
orgUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(null, false, EventSystemUser.SCIM);
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[orgUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WhenRevokingLastOwner_ReturnsErrorForThatUser(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser lastOwner)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
lastOwner.OrganizationId = organizationId;
|
||||||
|
lastOwner.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, true, null); // Is an owner
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[lastOwner],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(results);
|
||||||
|
Assert.True(results.First().IsError);
|
||||||
|
Assert.IsType<MustHaveConfirmedOwner>(results.First().AsError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_WithMultipleValidationErrors_ReturnsAllErrors(
|
||||||
|
SutProvider<RevokeOrganizationUsersValidator> sutProvider,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid actingUserId,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.User)] OrganizationUser revokedUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser ownerUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
revokedUser.OrganizationId = ownerUser.OrganizationId = organizationId;
|
||||||
|
revokedUser.UserId = Guid.NewGuid();
|
||||||
|
ownerUser.UserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
var actingUser = CreateActingUser(actingUserId, false, null); // Not an owner
|
||||||
|
var request = CreateValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
[revokedUser, ownerUser],
|
||||||
|
actingUser);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
|
||||||
|
.HasConfirmedOwnersExceptAsync(organizationId, Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = (await sutProvider.Sut.ValidateAsync(request)).ToList();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
Assert.All(results, r => Assert.True(r.IsError));
|
||||||
|
|
||||||
|
Assert.Contains(results, r => r.AsError is UserAlreadyRevoked);
|
||||||
|
Assert.Contains(results, r => r.AsError is OnlyOwnersCanRevokeOwners);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IActingUser CreateActingUser(Guid? userId, bool isOwnerOrProvider, EventSystemUser? systemUserType) =>
|
||||||
|
(userId, systemUserType) switch
|
||||||
|
{
|
||||||
|
({ } id, _) => new StandardUser(id, isOwnerOrProvider),
|
||||||
|
(null, { } type) => new SystemUser(type)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private static RevokeOrganizationUsersValidationRequest CreateValidationRequest(
|
||||||
|
Guid organizationId,
|
||||||
|
ICollection<OrganizationUser> organizationUsers,
|
||||||
|
IActingUser actingUser)
|
||||||
|
{
|
||||||
|
return new RevokeOrganizationUsersValidationRequest(
|
||||||
|
organizationId,
|
||||||
|
organizationUsers.Select(u => u.Id).ToList(),
|
||||||
|
actingUser,
|
||||||
|
organizationUsers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -8,6 +10,7 @@ using Bit.Core.Models.Data.Organizations;
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
@ -36,12 +39,16 @@ public class EventIntegrationHandlerTests
|
|||||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||||
{
|
{
|
||||||
var configurationCache = Substitute.For<IIntegrationConfigurationDetailsCache>();
|
var cache = Substitute.For<IFusionCache>();
|
||||||
configurationCache.GetConfigurationDetails(Arg.Any<Guid>(),
|
cache.GetOrSetAsync(
|
||||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Any<IEnumerable<string>>()
|
||||||
|
).Returns(configurations);
|
||||||
|
|
||||||
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
||||||
.SetDependency(configurationCache)
|
.SetDependency(cache)
|
||||||
.SetDependency(_eventIntegrationPublisher)
|
.SetDependency(_eventIntegrationPublisher)
|
||||||
.SetDependency(IntegrationType.Webhook)
|
.SetDependency(IntegrationType.Webhook)
|
||||||
.SetDependency(_logger)
|
.SetDependency(_logger)
|
||||||
@ -173,6 +180,37 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.ActingUser);
|
Assert.Null(context.ActingUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_ActingUserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
eventMessage.ActingUserId ??= Guid.NewGuid();
|
||||||
|
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.ActingUserId.Value).Returns(actingUser);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(actingUser);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithActingUser);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.ActingUserId.Value);
|
||||||
|
Assert.Equal(actingUser, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
public async Task BuildContextAsync_GroupIdPresent_UsesCache(EventMessage eventMessage, Group group)
|
||||||
{
|
{
|
||||||
@ -211,6 +249,32 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.Group);
|
Assert.Null(context.Group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_GroupFactory_CallsGroupRepository(EventMessage eventMessage, Group group)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var groupRepository = sutProvider.GetDependency<IGroupRepository>();
|
||||||
|
|
||||||
|
eventMessage.GroupId ??= Guid.NewGuid();
|
||||||
|
groupRepository.GetByIdAsync(eventMessage.GroupId.Value).Returns(group);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Group?>, CancellationToken, Task<Group?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(group);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithGroup);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await groupRepository.Received(1).GetByIdAsync(eventMessage.GroupId.Value);
|
||||||
|
Assert.Equal(group, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
public async Task BuildContextAsync_OrganizationIdPresent_UsesCache(EventMessage eventMessage, Organization organization)
|
||||||
{
|
{
|
||||||
@ -250,6 +314,32 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.Organization);
|
Assert.Null(context.Organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_OrganizationFactory_CallsOrganizationRepository(EventMessage eventMessage, Organization organization)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value).Returns(organization);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<Organization?>, CancellationToken, Task<Organization?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithOrganization);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationRepository.Received(1).GetByIdAsync(eventMessage.OrganizationId.Value);
|
||||||
|
Assert.Equal(organization, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
public async Task BuildContextAsync_UserIdPresent_UsesCache(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||||
{
|
{
|
||||||
@ -313,6 +403,38 @@ public class EventIntegrationHandlerTests
|
|||||||
Assert.Null(context.User);
|
Assert.Null(context.User);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task BuildContextAsync_UserFactory_CallsOrganizationUserRepository(EventMessage eventMessage, OrganizationUserUserDetails userDetails)
|
||||||
|
{
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
eventMessage.OrganizationId ??= Guid.NewGuid();
|
||||||
|
eventMessage.UserId ??= Guid.NewGuid();
|
||||||
|
organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.UserId.Value).Returns(userDetails);
|
||||||
|
|
||||||
|
// Capture the factory function passed to the cache
|
||||||
|
Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<OrganizationUserUserDetails?>, CancellationToken, Task<OrganizationUserUserDetails?>>>(f => capturedFactory = f)
|
||||||
|
).Returns(userDetails);
|
||||||
|
|
||||||
|
await sutProvider.Sut.BuildContextAsync(eventMessage, _templateWithUser);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
var result = await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await organizationUserRepository.Received(1).GetDetailsByOrganizationIdUserIdAsync(
|
||||||
|
eventMessage.OrganizationId.Value,
|
||||||
|
eventMessage.UserId.Value);
|
||||||
|
Assert.Equal(userDetails, result);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
public async Task BuildContextAsync_NoSpecialTokens_DoesNotCallAnyCache(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
@ -344,6 +466,12 @@ public class EventIntegrationHandlerTests
|
|||||||
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(NoConfigurations());
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
cache.GetOrSetAsync<List<OrganizationIntegrationConfigurationDetails>>(
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<Func<object, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
Arg.Any<FusionCacheEntryOptions>()
|
||||||
|
).Returns(NoConfigurations());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
Assert.Empty(_eventIntegrationPublisher.ReceivedCalls());
|
||||||
@ -362,8 +490,8 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
|
||||||
eventMessage.OrganizationId = _organizationId;
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@ -382,8 +510,8 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
|
||||||
eventMessage.OrganizationId = _organizationId;
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@ -405,6 +533,7 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_FilterReturnsFalse_DoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(false);
|
||||||
@ -416,10 +545,10 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
|
public async Task HandleEventAsync_FilterReturnsTrue_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||||
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||||
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
||||||
eventMessage.OrganizationId = _organizationId;
|
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
@ -435,6 +564,7 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
|
public async Task HandleEventAsync_InvalidFilter_LogsErrorDoesNothing(EventMessage eventMessage)
|
||||||
{
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
|
var sutProvider = GetSutProvider(InvalidFilterConfiguration());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
@ -444,12 +574,13 @@ public class EventIntegrationHandlerTests
|
|||||||
Arg.Any<EventId>(),
|
Arg.Any<EventId>(),
|
||||||
Arg.Any<object>(),
|
Arg.Any<object>(),
|
||||||
Arg.Any<JsonException>(),
|
Arg.Any<JsonException>(),
|
||||||
Arg.Any<Func<object, Exception, string>>());
|
Arg.Any<Func<object, Exception?, string>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(NoConfigurations());
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
@ -459,13 +590,14 @@ public class EventIntegrationHandlerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
|
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessages(List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
|
|
||||||
foreach (var eventMessage in eventMessages)
|
foreach (var eventMessage in eventMessages)
|
||||||
{
|
{
|
||||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
var expectedMessage = ExpectedMessage(
|
||||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||||
);
|
);
|
||||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||||
@ -477,13 +609,14 @@ public class EventIntegrationHandlerTests
|
|||||||
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(
|
||||||
List<EventMessage> eventMessages)
|
List<EventMessage> eventMessages)
|
||||||
{
|
{
|
||||||
|
eventMessages.ForEach(e => e.OrganizationId = _organizationId);
|
||||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||||
|
|
||||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||||
|
|
||||||
foreach (var eventMessage in eventMessages)
|
foreach (var eventMessage in eventMessages)
|
||||||
{
|
{
|
||||||
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
|
var expectedMessage = ExpectedMessage(
|
||||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||||
);
|
);
|
||||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
|
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||||
@ -494,4 +627,84 @@ public class EventIntegrationHandlerTests
|
|||||||
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_CapturedFactories_CallConfigurationRepository(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
var configurationRepository = sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>();
|
||||||
|
|
||||||
|
var configs = OneConfiguration(_templateBase);
|
||||||
|
|
||||||
|
configurationRepository.GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook).Returns(configs);
|
||||||
|
|
||||||
|
// Capture the factory function - there will be 1 call that returns both specific and wildcard matches
|
||||||
|
Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>? capturedFactory = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Do<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(f
|
||||||
|
=> capturedFactory = f),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Any<IEnumerable<string>>()
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
// Verify factory was captured
|
||||||
|
Assert.NotNull(capturedFactory);
|
||||||
|
|
||||||
|
// Execute the captured factory to trigger repository call
|
||||||
|
await capturedFactory(null!, CancellationToken.None);
|
||||||
|
|
||||||
|
await configurationRepository.Received(1).GetManyByEventTypeOrganizationIdIntegrationType(eventType: eventMessage.Type, organizationId: _organizationId, integrationType: IntegrationType.Webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_ConfigurationCacheOptions_SetsDurationToConstant(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
|
||||||
|
FusionCacheEntryOptions? capturedOption = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Do<FusionCacheEntryOptions>(opt => capturedOption = opt),
|
||||||
|
tags: Arg.Any<IEnumerable<string>?>()
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
Assert.NotNull(capturedOption);
|
||||||
|
Assert.Equal(EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails,
|
||||||
|
capturedOption.Duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task HandleEventAsync_ConfigurationCache_AddsOrganizationIntegrationTag(EventMessage eventMessage)
|
||||||
|
{
|
||||||
|
eventMessage.OrganizationId = _organizationId;
|
||||||
|
var sutProvider = GetSutProvider(NoConfigurations());
|
||||||
|
var cache = sutProvider.GetDependency<IFusionCache>();
|
||||||
|
|
||||||
|
IEnumerable<string>? capturedTags = null;
|
||||||
|
cache.GetOrSetAsync(
|
||||||
|
key: Arg.Any<string>(),
|
||||||
|
factory: Arg.Any<Func<FusionCacheFactoryExecutionContext<List<OrganizationIntegrationConfigurationDetails>>, CancellationToken, Task<List<OrganizationIntegrationConfigurationDetails>>>>(),
|
||||||
|
options: Arg.Any<FusionCacheEntryOptions>(),
|
||||||
|
tags: Arg.Do<IEnumerable<string>>(t => capturedTags = t)
|
||||||
|
).Returns(new List<OrganizationIntegrationConfigurationDetails>());
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||||
|
|
||||||
|
var expectedTag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
_organizationId,
|
||||||
|
IntegrationType.Webhook
|
||||||
|
);
|
||||||
|
Assert.NotNull(capturedTags);
|
||||||
|
Assert.Contains(expectedTag, capturedTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,173 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Data.Organizations;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using NSubstitute.ExceptionExtensions;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
|
||||||
|
|
||||||
[SutProviderCustomize]
|
|
||||||
public class IntegrationConfigurationDetailsCacheServiceTests
|
|
||||||
{
|
|
||||||
private SutProvider<IntegrationConfigurationDetailsCacheService> GetSutProvider(
|
|
||||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
|
||||||
{
|
|
||||||
var configurationRepository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
|
||||||
configurationRepository.GetAllConfigurationDetailsAsync().Returns(configurations);
|
|
||||||
|
|
||||||
return new SutProvider<IntegrationConfigurationDetailsCacheService>()
|
|
||||||
.SetDependency(configurationRepository)
|
|
||||||
.Create();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_SpecificKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
config.EventType = EventType.Cipher_Created;
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Same(config, result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_AllEventsKeyExists_ReturnsExpectedList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
config.EventType = null;
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Same(config, result[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_BothSpecificAndAllEventsKeyExists_ReturnsExpectedList(
|
|
||||||
OrganizationIntegrationConfigurationDetails specificConfig,
|
|
||||||
OrganizationIntegrationConfigurationDetails allKeysConfig
|
|
||||||
)
|
|
||||||
{
|
|
||||||
specificConfig.EventType = EventType.Cipher_Created;
|
|
||||||
allKeysConfig.EventType = null;
|
|
||||||
allKeysConfig.OrganizationId = specificConfig.OrganizationId;
|
|
||||||
allKeysConfig.IntegrationType = specificConfig.IntegrationType;
|
|
||||||
|
|
||||||
var sutProvider = GetSutProvider([specificConfig, allKeysConfig]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
specificConfig.OrganizationId,
|
|
||||||
specificConfig.IntegrationType,
|
|
||||||
EventType.Cipher_Created);
|
|
||||||
Assert.Equal(2, result.Count);
|
|
||||||
Assert.Contains(result, r => r.Template == specificConfig.Template);
|
|
||||||
Assert.Contains(result, r => r.Template == allKeysConfig.Template);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_KeyMissing_ReturnsEmptyList(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
Guid.NewGuid(),
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Empty(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetConfigurationDetails_ReturnsCachedValue_EvenIfRepositoryChanges(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
var newConfig = JsonSerializer.Deserialize<OrganizationIntegrationConfigurationDetails>(JsonSerializer.Serialize(config));
|
|
||||||
Assert.NotNull(newConfig);
|
|
||||||
newConfig.Template = "Changed";
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().GetAllConfigurationDetailsAsync()
|
|
||||||
.Returns([newConfig]);
|
|
||||||
|
|
||||||
var result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.NotEqual("Changed", result[0].Template); // should not yet pick up change from repository
|
|
||||||
|
|
||||||
await sutProvider.Sut.RefreshAsync(); // Pick up changes
|
|
||||||
|
|
||||||
result = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config.OrganizationId,
|
|
||||||
config.IntegrationType,
|
|
||||||
config.EventType ?? EventType.Cipher_Created);
|
|
||||||
Assert.Single(result);
|
|
||||||
Assert.Equal("Changed", result[0].Template); // Should have the new value
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RefreshAsync_GroupsByCompositeKey(OrganizationIntegrationConfigurationDetails config1)
|
|
||||||
{
|
|
||||||
var config2 = JsonSerializer.Deserialize<OrganizationIntegrationConfigurationDetails>(
|
|
||||||
JsonSerializer.Serialize(config1))!;
|
|
||||||
config2.Template = "Another";
|
|
||||||
|
|
||||||
var sutProvider = GetSutProvider([config1, config2]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
var results = sutProvider.Sut.GetConfigurationDetails(
|
|
||||||
config1.OrganizationId,
|
|
||||||
config1.IntegrationType,
|
|
||||||
config1.EventType ?? EventType.Cipher_Created);
|
|
||||||
|
|
||||||
Assert.Equal(2, results.Count);
|
|
||||||
Assert.Contains(results, r => r.Template == config1.Template);
|
|
||||||
Assert.Contains(results, r => r.Template == config2.Template);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RefreshAsync_LogsInformationOnSuccess(OrganizationIntegrationConfigurationDetails config)
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([config]);
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ILogger<IntegrationConfigurationDetailsCacheService>>().Received().Log(
|
|
||||||
LogLevel.Information,
|
|
||||||
Arg.Any<EventId>(),
|
|
||||||
Arg.Is<object>(o => o.ToString()!.Contains("Refreshed successfully")),
|
|
||||||
null,
|
|
||||||
Arg.Any<Func<object, Exception?, string>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RefreshAsync_OnException_LogsError()
|
|
||||||
{
|
|
||||||
var sutProvider = GetSutProvider([]);
|
|
||||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().GetAllConfigurationDetailsAsync()
|
|
||||||
.Throws(new Exception("Database failure"));
|
|
||||||
await sutProvider.Sut.RefreshAsync();
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ILogger<IntegrationConfigurationDetailsCacheService>>().Received(1).Log(
|
|
||||||
LogLevel.Error,
|
|
||||||
Arg.Any<EventId>(),
|
|
||||||
Arg.Is<object>(o => o.ToString()!.Contains("Refresh failed")),
|
|
||||||
Arg.Any<Exception>(),
|
|
||||||
Arg.Any<Func<object, Exception?, string>>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1017,6 +1017,7 @@ public class RegisterUserCommandTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually)]
|
[BitAutoData(PlanType.FamiliesAnnually)]
|
||||||
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
[BitAutoData(PlanType.FamiliesAnnually2019)]
|
||||||
|
[BitAutoData(PlanType.FamiliesAnnually2025)]
|
||||||
[BitAutoData(PlanType.Free)]
|
[BitAutoData(PlanType.Free)]
|
||||||
public async Task SendWelcomeEmail_FamilyOrg_SendsFamilyWelcomeEmail(
|
public async Task SendWelcomeEmail_FamilyOrg_SendsFamilyWelcomeEmail(
|
||||||
PlanType planType,
|
PlanType planType,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -11,8 +12,12 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"Group:{groupId:N}";
|
var expected = $"Group:{groupId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||||
|
var keyWithDifferentGroup = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(Guid.NewGuid());
|
||||||
|
var keyWithSameGroup = EventIntegrationsCacheConstants.BuildCacheKeyForGroup(groupId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentGroup);
|
||||||
|
Assert.Equal(key, keyWithSameGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -20,8 +25,69 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"Organization:{orgId:N}";
|
var expected = $"Organization:{orgId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||||
|
var keyWithDifferentOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(Guid.NewGuid());
|
||||||
|
var keyWithSameOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganization(orgId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentOrg);
|
||||||
|
Assert.Equal(key, keyWithSameOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildCacheKeyForOrganizationIntegrationConfigurationDetails_ReturnsExpectedKey(Guid orgId)
|
||||||
|
{
|
||||||
|
var integrationType = IntegrationType.Hec;
|
||||||
|
|
||||||
|
var expectedWithEvent = $"OrganizationIntegrationConfigurationDetails:{orgId:N}:Hec:User_LoggedIn";
|
||||||
|
var keyWithEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.User_LoggedIn);
|
||||||
|
var keyWithDifferentEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.Cipher_Created);
|
||||||
|
var keyWithDifferentIntegration = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, IntegrationType.Webhook, EventType.User_LoggedIn);
|
||||||
|
var keyWithDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid.NewGuid(), integrationType, EventType.User_LoggedIn);
|
||||||
|
var keyWithSameDetails = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, EventType.User_LoggedIn);
|
||||||
|
|
||||||
|
Assert.Equal(expectedWithEvent, keyWithEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentIntegration);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithDifferentOrganization);
|
||||||
|
Assert.Equal(keyWithEvent, keyWithSameDetails);
|
||||||
|
|
||||||
|
var expectedWithNullEvent = $"OrganizationIntegrationConfigurationDetails:{orgId:N}:Hec:";
|
||||||
|
var keyWithNullEvent = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, integrationType, null);
|
||||||
|
var keyWithNullEventDifferentIntegration = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
orgId, IntegrationType.Webhook, null);
|
||||||
|
var keyWithNullEventDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationIntegrationConfigurationDetails(
|
||||||
|
Guid.NewGuid(), integrationType, null);
|
||||||
|
|
||||||
|
Assert.Equal(expectedWithNullEvent, keyWithNullEvent);
|
||||||
|
Assert.NotEqual(keyWithEvent, keyWithNullEvent);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithDifferentEvent);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentIntegration);
|
||||||
|
Assert.NotEqual(keyWithNullEvent, keyWithNullEventDifferentOrganization);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void BuildCacheTagForOrganizationIntegration_ReturnsExpectedKey(Guid orgId)
|
||||||
|
{
|
||||||
|
var expected = $"OrganizationIntegration:{orgId:N}:Hec";
|
||||||
|
var tag = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Hec);
|
||||||
|
var tagWithDifferentOrganization = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
Guid.NewGuid(), IntegrationType.Hec);
|
||||||
|
var tagWithDifferentIntegrationType = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Webhook);
|
||||||
|
var tagWithSameDetails = EventIntegrationsCacheConstants.BuildCacheTagForOrganizationIntegration(
|
||||||
|
orgId, IntegrationType.Hec);
|
||||||
|
|
||||||
|
Assert.Equal(expected, tag);
|
||||||
|
Assert.NotEqual(tag, tagWithDifferentOrganization);
|
||||||
|
Assert.NotEqual(tag, tagWithDifferentIntegrationType);
|
||||||
|
Assert.Equal(tag, tagWithSameDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -29,8 +95,14 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
var expected = $"OrganizationUserUserDetails:{orgId:N}:{userId:N}";
|
||||||
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
var key = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||||
|
var keyWithDifferentOrg = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(Guid.NewGuid(), userId);
|
||||||
|
var keyWithDifferentUser = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, Guid.NewGuid());
|
||||||
|
var keyWithSameDetails = EventIntegrationsCacheConstants.BuildCacheKeyForOrganizationUser(orgId, userId);
|
||||||
|
|
||||||
Assert.Equal(expected, key);
|
Assert.Equal(expected, key);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentOrg);
|
||||||
|
Assert.NotEqual(key, keyWithDifferentUser);
|
||||||
|
Assert.Equal(key, keyWithSameDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -38,4 +110,13 @@ public class EventIntegrationsCacheConstantsTests
|
|||||||
{
|
{
|
||||||
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
Assert.Equal("EventIntegrations", EventIntegrationsCacheConstants.CacheName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DurationForOrganizationIntegrationConfigurationDetails_ReturnsExpected()
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
TimeSpan.FromDays(1),
|
||||||
|
EventIntegrationsCacheConstants.DurationForOrganizationIntegrationConfigurationDetails
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@IntegrationType SMALLINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
oic.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
|
WHERE
|
||||||
|
(oic.[EventType] = @EventType OR oic.[EventType] IS NULL)
|
||||||
|
AND
|
||||||
|
oic.[OrganizationId] = @OrganizationId
|
||||||
|
AND
|
||||||
|
oic.[IntegrationType] = @IntegrationType
|
||||||
|
END
|
||||||
|
GO
|
||||||
Loading…
x
Reference in New Issue
Block a user