1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-26 05:38:47 -05:00

Merge branch 'main' into dirt/pm-20112_member_access_report_503error

This commit is contained in:
Graham Walker 2025-05-29 11:33:49 -05:00 committed by GitHub
commit 53e9f50682
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1397 additions and 1110 deletions

View File

@ -44,6 +44,7 @@ jobs:
with:
accessToken: ${{ secrets.LD_ACCESS_TOKEN }}
projKey: default
allowTags: true
- name: Add label
if: steps.collect.outputs.any-changed == 'true'

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.5.1</Version>
<Version>2025.5.2</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@ -5,9 +5,6 @@
<a href="https://github.com/bitwarden/server/actions/workflows/build.yml?query=branch:main" target="_blank">
<img src="https://github.com/bitwarden/server/actions/workflows/build.yml/badge.svg?branch=main" alt="Github Workflow build on main" />
</a>
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<img src="https://img.shields.io/docker/pulls/bitwarden/api.svg" alt="DockerHub" />
</a>
<a href="https://gitter.im/bitwarden/Lobby" target="_blank">
<img src="https://badges.gitter.im/bitwarden/Lobby.svg" alt="gitter chat" />
</a>
@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett
## Deploy
<p align="center">
<a href="https://hub.docker.com/u/bitwarden/" target="_blank">
<a href="https://github.com/orgs/bitwarden/packages" target="_blank">
<img src="https://i.imgur.com/SZc8JnH.png" alt="docker" />
</a>
</p>
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [Docker Hub](https://hub.docker.com/u/bitwarden/).
You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages).
Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/

View File

@ -33,6 +33,39 @@
"Name": "events-webhook-subscription"
}
]
},
{
"Name": "event-integrations",
"Subscriptions": [
{
"Name": "integration-slack-subscription",
"Rules": [
{
"Name": "slack-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "slack"
}
}
}
]
},
{
"Name": "integration-webhook-subscription",
"Rules": [
{
"Name": "webhook-integration-filter",
"Properties": {
"FilterType": "Correlation",
"CorrelationFilter": {
"Label": "webhook"
}
}
}
]
}
]
}
]
}

View File

@ -44,6 +44,8 @@ public class OrganizationViewModel
orgUsers
.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus)
.Select(u => u.Email));
OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus);
AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus);
SecretsCount = secretsCount;
ProjectsCount = projectCount;
ServiceAccountsCount = serviceAccountsCount;
@ -70,4 +72,6 @@ public class OrganizationViewModel
public int OccupiedSmSeatsCount { get; set; }
public bool UseSecretsManager => Organization.UseSecretsManager;
public bool UseRiskInsights => Organization.UseRiskInsights;
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
}

View File

@ -19,12 +19,6 @@
<span id="org-confirmed-users" title="Confirmed">@Model.UserConfirmedCount</span>)
</dd>
<dt class="col-sm-4 col-lg-3">Owners</dt>
<dd id="org-owner" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)</dd>
<dt class="col-sm-4 col-lg-3">Admins</dt>
<dd id="org-admins" class="col-sm-8 col-lg-9">@(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)</dd>
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
<dd id="org-2fa" class="col-sm-8 col-lg-9">@(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")</dd>
@ -76,3 +70,49 @@
<dt class="col-sm-4 col-lg-3">Secrets Manager Seats</dt>
<dd id="sm-seat-count" class="col-sm-8 col-lg-9">@(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )</dd>
</dl>
<h2>Administrators</h2>
<dl class="row">
<div class="table-responsive">
<div class="col-8">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 190px;">Email</th>
<th style="width: 60px;">Role</th>
<th style="width: 40px;">Status</th>
</tr>
</thead>
<tbody>
@if(!Model.Admins.Any() && !Model.Owners.Any())
{
<tr>
<td colspan="6">No results to list.</td>
</tr>
}
else
{
@foreach(var owner in Model.OwnersDetails)
{
<tr>
<td class="align-middle">@owner.Email</td>
<td class="align-middle">Owner</td>
<td class="align-middle">@owner.Status</td>
</tr>
}
@foreach(var admin in Model.AdminsDetails)
{
<tr>
<td class="align-middle">@admin.Email</td>
<td class="align-middle">Admin</td>
<td class="align-middle">@admin.Status</td>
</tr>
}
}
</tbody>
</table>
</div>
</div>
</dl>

View File

@ -12,7 +12,8 @@ public record OrganizationMetadataResponse(
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate)
DateTime? SubPeriodEndDate,
int OrganizationOccupiedSeats)
{
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
=> new(
@ -25,5 +26,6 @@ public record OrganizationMetadataResponse(
metadata.IsSubscriptionCanceled,
metadata.InvoiceDueDate,
metadata.InvoiceCreatedDate,
metadata.SubPeriodEndDate);
metadata.SubPeriodEndDate,
metadata.OrganizationOccupiedSeats);
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@ -27,6 +29,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
private readonly IUserRepository _userRepository;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public AcceptOrgUserCommand(
IDataProtectionProvider dataProtectionProvider,
@ -37,9 +41,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
IMailService mailService,
IUserRepository userRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery)
{
// TODO: remove data protector when old token validation removed
_dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose);
_globalSettings = globalSettings;
@ -50,6 +55,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
_userRepository = userRepository;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_policyRequirementQuery = policyRequirementQuery;
}
public async Task<OrganizationUser> AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken,
@ -196,15 +203,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}
await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);
orgUser.Status = OrganizationUserStatusType.Accepted;
orgUser.UserId = user.Id;
@ -224,4 +223,33 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
return orgUser;
}
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
// If the user has two-step login enabled, we skip checking the 2FA policy
return;
}
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
return;
}
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))
{
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
}
}
}
}

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
private readonly IPushRegistrationService _pushRegistrationService;
private readonly IPolicyService _policyService;
private readonly IDeviceRepository _deviceRepository;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IFeatureService _featureService;
public ConfirmOrganizationUserCommand(
IOrganizationRepository organizationRepository,
@ -35,7 +39,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService,
IPolicyService policyService,
IDeviceRepository deviceRepository)
IDeviceRepository deviceRepository,
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -47,6 +53,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
_pushRegistrationService = pushRegistrationService;
_policyService = policyService;
_deviceRepository = deviceRepository;
_policyRequirementQuery = policyRequirementQuery;
_featureService = featureService;
}
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
@ -118,8 +126,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
}
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;
@ -142,15 +150,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
private async Task CheckPoliciesAsync(Guid organizationId, User user,
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
ICollection<OrganizationUser> userOrgs, bool userTwoFactorEnabled)
{
// Enforce Two Factor Authentication Policy for this organization
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
.Any(p => p.OrganizationId == organizationId);
if (orgRequiresTwoFactor && !twoFactorEnabled)
{
throw new BadRequestException("User does not have two-step login enabled.");
}
await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled);
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
@ -168,6 +171,33 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
}
}
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
if (userTwoFactorEnabled)
{
// If the user has two-step login enabled, we skip checking the 2FA policy
return;
}
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
{
throw new BadRequestException("User does not have two-step login enabled.");
}
return;
}
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
.Any(p => p.OrganizationId == organizationId);
if (orgRequiresTwoFactor && !userTwoFactorEnabled)
{
throw new BadRequestException("User does not have two-step login enabled.");
}
}
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var devices = await GetUserDeviceIdsAsync(userId);

View File

@ -1,5 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand(
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IPolicyService policyService,
IUserRepository userRepository,
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
IOrganizationService organizationService,
IFeatureService featureService,
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
{
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
{
@ -270,12 +274,7 @@ public class RestoreOrganizationUserCommand(
// Enforce 2FA Policy of organization user is trying to join
if (!userHasTwoFactorEnabled)
{
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
twoFactorCompliant = false;
}
twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId);
}
var user = await userRepository.GetByIdAsync(userId);
@ -299,4 +298,17 @@ public class RestoreOrganizationUserCommand(
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
}
}
private async Task<bool> IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId)
{
if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);
return requirement.IsTwoFactorRequiredForOrganization(organizationId);
}
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId);
}
}

View File

@ -0,0 +1,52 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
/// <summary>
/// Policy requirements for the Require Two-Factor Authentication policy.
/// </summary>
public class RequireTwoFactorPolicyRequirement : IPolicyRequirement
{
private readonly IEnumerable<PolicyDetails> _policyDetails;
public RequireTwoFactorPolicyRequirement(IEnumerable<PolicyDetails> policyDetails)
{
_policyDetails = policyDetails;
}
/// <summary>
/// Checks if two-factor authentication is required for the organization due to an active policy.
/// </summary>
/// <param name="organizationId">The ID of the organization to check.</param>
/// <returns>True if two-factor authentication is required for the organization, false otherwise.</returns>
/// <remarks>
/// This should be used to check whether the member needs to have 2FA enabled before being
/// accepted, confirmed, or restored to the organization.
/// </remarks>
public bool IsTwoFactorRequiredForOrganization(Guid organizationId) =>
_policyDetails.Any(p => p.OrganizationId == organizationId);
/// <summary>
/// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required.
/// Users should be revoked from these organizations if they disable all 2FA methods.
/// </summary>
public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor =>
_policyDetails
.Where(p => p.OrganizationUserStatus is
OrganizationUserStatusType.Accepted or
OrganizationUserStatusType.Confirmed)
.Select(p => (p.OrganizationId, p.OrganizationUserId));
}
public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory<RequireTwoFactorPolicyRequirement>
{
public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication;
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => [];
public override RequireTwoFactorPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
{
return new RequireTwoFactorPolicyRequirement(policyDetails);
}
}

View File

@ -36,5 +36,6 @@ public static class PolicyServiceCollectionExtensions
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
}
}

View File

@ -20,7 +20,7 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService
string subscriptionName) : base(handler)
{
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
_processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions());
_processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions());
_logger = logger;
}

View File

@ -14,7 +14,7 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos
public AzureServiceBusEventWriteService(GlobalSettings globalSettings)
{
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName);
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName);
}
public async Task CreateAsync(IEvent e)

View File

@ -0,0 +1,101 @@
#nullable enable
using Azure.Messaging.ServiceBus;
using Bit.Core.Settings;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
public class AzureServiceBusIntegrationListenerService : BackgroundService
{
private readonly int _maxRetries;
private readonly string _subscriptionName;
private readonly string _topicName;
private readonly IIntegrationHandler _handler;
private readonly ServiceBusClient _client;
private readonly ServiceBusProcessor _processor;
private readonly ServiceBusSender _sender;
private readonly ILogger<AzureServiceBusIntegrationListenerService> _logger;
public AzureServiceBusIntegrationListenerService(
IIntegrationHandler handler,
string subscriptionName,
GlobalSettings globalSettings,
ILogger<AzureServiceBusIntegrationListenerService> logger)
{
_handler = handler;
_logger = logger;
_maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries;
_topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName;
_subscriptionName = subscriptionName;
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
_processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions());
_sender = _client.CreateSender(_topicName);
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
_processor.ProcessMessageAsync += HandleMessageAsync;
_processor.ProcessErrorAsync += args =>
{
_logger.LogError(args.Exception, "Azure Service Bus error");
return Task.CompletedTask;
};
await _processor.StartProcessingAsync(cancellationToken);
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
await _processor.StopProcessingAsync(cancellationToken);
await _processor.DisposeAsync();
await _sender.DisposeAsync();
await _client.DisposeAsync();
await base.StopAsync(cancellationToken);
}
private async Task HandleMessageAsync(ProcessMessageEventArgs args)
{
var json = args.Message.Body.ToString();
try
{
var result = await _handler.HandleAsync(json);
var message = result.Message;
if (result.Success)
{
await args.CompleteMessageAsync(args.Message);
return;
}
message.ApplyRetry(result.DelayUntilDate);
if (result.Retryable && message.RetryCount < _maxRetries)
{
var scheduledTime = (DateTime)message.DelayUntilDate!;
var retryMsg = new ServiceBusMessage(message.ToJson())
{
Subject = args.Message.Subject,
ScheduledEnqueueTime = scheduledTime
};
await _sender.SendMessageAsync(retryMsg);
}
else
{
await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable");
return;
}
await args.CompleteMessageAsync(args.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled error processing ASB message");
await args.CompleteMessageAsync(args.Message);
}
}
}

View File

@ -0,0 +1,36 @@
using Azure.Messaging.ServiceBus;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
using Bit.Core.Settings;
namespace Bit.Core.Services;
public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable
{
private readonly ServiceBusClient _client;
private readonly ServiceBusSender _sender;
public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings)
{
_client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString);
_sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName);
}
public async Task PublishAsync(IIntegrationMessage message)
{
var json = message.ToJson();
var serviceBusMessage = new ServiceBusMessage(json)
{
Subject = message.IntegrationType.ToRoutingKey(),
};
await _sender.SendMessageAsync(serviceBusMessage);
}
public async ValueTask DisposeAsync()
{
await _sender.DisposeAsync();
await _client.DisposeAsync();
}
}

View File

@ -1,66 +0,0 @@
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
namespace Bit.Core.Services;
public abstract class IntegrationEventHandlerBase(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IEventMessageHandler
{
public async Task HandleEventAsync(EventMessage eventMessage)
{
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
organizationId,
GetIntegrationType(),
eventMessage.Type);
foreach (var configuration in configurations)
{
var context = await BuildContextAsync(eventMessage, configuration.Template);
var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context);
await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate);
}
}
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
{
foreach (var eventMessage in eventMessages)
{
await HandleEventAsync(eventMessage);
}
}
private async Task<IntegrationTemplateContext> BuildContextAsync(EventMessage eventMessage, string template)
{
var context = new IntegrationTemplateContext(eventMessage);
if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue)
{
context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value);
}
if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue)
{
context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value);
}
if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue)
{
context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value);
}
return context;
}
protected abstract IntegrationType GetIntegrationType();
protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate);
}

View File

@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
@ -45,7 +44,6 @@ public class OrganizationService : IOrganizationService
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly ICollectionRepository _collectionRepository;
private readonly IUserRepository _userRepository;
private readonly IGroupRepository _groupRepository;
private readonly IMailService _mailService;
private readonly IPushNotificationService _pushNotificationService;
@ -69,7 +67,6 @@ public class OrganizationService : IOrganizationService
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IProviderRepository _providerRepository;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
@ -79,7 +76,6 @@ public class OrganizationService : IOrganizationService
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository,
IUserRepository userRepository,
IGroupRepository groupRepository,
IMailService mailService,
IPushNotificationService pushNotificationService,
@ -103,7 +99,6 @@ public class OrganizationService : IOrganizationService
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IProviderRepository providerRepository,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery,
@ -113,7 +108,6 @@ public class OrganizationService : IOrganizationService
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_collectionRepository = collectionRepository;
_userRepository = userRepository;
_groupRepository = groupRepository;
_mailService = mailService;
_pushNotificationService = pushNotificationService;
@ -137,7 +131,6 @@ public class OrganizationService : IOrganizationService
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_providerRepository = providerRepository;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
_policyRequirementQuery = policyRequirementQuery;
@ -1722,72 +1715,6 @@ public class OrganizationService : IOrganizationService
return result;
}
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
{
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
// The user will be subject to the same checks when they try to accept the invite
if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
{
return;
}
var userId = orgUser.UserId.Value;
// Enforce Single Organization Policy of organization user is being restored to
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
var singleOrgCompliant = true;
var belongsToOtherOrgCompliant = true;
var twoFactorCompliant = true;
if (hasOtherOrgs && singleOrgPolicyApplies)
{
singleOrgCompliant = false;
}
// Enforce Single Organization Policy of other organizations user is a member of
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId,
PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
belongsToOtherOrgCompliant = false;
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
if (!userHasTwoFactorEnabled)
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
twoFactorCompliant = false;
}
}
var user = await _userRepository.GetByIdAsync(userId);
if (!singleOrgCompliant && !twoFactorCompliant)
{
throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy");
}
else if (!singleOrgCompliant)
{
throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
}
else if (!belongsToOtherOrgCompliant)
{
throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations");
}
else if (!twoFactorCompliant)
{
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
}
}
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
{
// Determine status to revert back to

View File

@ -1,35 +0,0 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
using Bit.Core.Repositories;
#nullable enable
namespace Bit.Core.Services;
public class SlackEventHandler(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository,
ISlackService slackService)
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
{
protected override IntegrationType GetIntegrationType() => IntegrationType.Slack;
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate)
{
var config = mergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
if (config is null)
{
return;
}
await slackService.SendSlackMessageByChannelIdAsync(
config.token,
renderedTemplate,
config.channelId
);
}
}

View File

@ -1,38 +0,0 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
using Bit.Core.Repositories;
#nullable enable
namespace Bit.Core.Services;
public class WebhookEventHandler(
IHttpClientFactory httpClientFactory,
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository)
{
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
public const string HttpClientName = "WebhookEventHandlerHttpClient";
protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook;
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate)
{
var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetails>();
if (config is null || string.IsNullOrEmpty(config.url))
{
return;
}
var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(config.url, content);
response.EnsureSuccessStatusCode();
}
}

View File

@ -10,7 +10,8 @@ public record OrganizationMetadata(
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate)
DateTime? SubPeriodEndDate,
int OrganizationOccupiedSeats)
{
public static OrganizationMetadata Default => new OrganizationMetadata(
false,
@ -22,5 +23,6 @@ public record OrganizationMetadata(
false,
null,
null,
null);
null,
0);
}

View File

@ -31,6 +31,7 @@ public class OrganizationBillingService(
IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPricingClient pricingClient,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
@ -107,6 +108,8 @@ public class OrganizationBillingService(
? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions())
: null;
var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
return new OrganizationMetadata(
isEligibleForSelfHost,
isManaged,
@ -117,7 +120,8 @@ public class OrganizationBillingService(
subscription.Status == StripeConstants.SubscriptionStatus.Canceled,
invoice?.DueDate,
invoice?.Created,
subscription.CurrentPeriodEnd);
subscription.CurrentPeriodEnd,
orgOccupiedSeats);
}
public async Task

View File

@ -109,6 +109,8 @@ public static class FeatureFlagKeys
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript";
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
/* Auth Team */
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
@ -200,7 +202,6 @@ public static class FeatureFlagKeys
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
public const string EndUserNotifications = "pm-10609-end-user-notifications";
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
public const string PhishingDetection = "phishing-detection";
public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";

View File

@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -81,6 +83,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
public UserService(
IUserRepository userRepository,
@ -119,7 +122,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache)
IDistributedCache distributedCache,
IPolicyRequirementQuery policyRequirementQuery)
: base(
store,
optionsAccessor,
@ -164,6 +168,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache;
_policyRequirementQuery = policyRequirementQuery;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@ -1394,9 +1399,40 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
{
var requirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
if (!requirement.OrganizationsRequiringTwoFactor.Any())
{
Logger.LogInformation("No organizations requiring two factor for user {userId}.", user.Id);
return;
}
var organizationIds = requirement.OrganizationsRequiringTwoFactor.Select(o => o.OrganizationId).ToList();
var organizations = await _organizationRepository.GetManyByIdsAsync(organizationIds);
var organizationLookup = organizations.ToDictionary(org => org.Id);
var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor
.Where(o => organizationLookup.ContainsKey(o.OrganizationId))
.Select(async o =>
{
var organization = organizationLookup[o.OrganizationId];
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
new RevokeOrganizationUsersRequest(
o.OrganizationId,
[new OrganizationUserUserDetails { Id = o.OrganizationUserId, OrganizationId = o.OrganizationId }],
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}).ToArray();
await Task.WhenAll(revokeOrgUserTasks);
return;
}
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
var legacyRevokeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
@ -1407,7 +1443,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}).ToArray();
await Task.WhenAll(removeOrgUserTasks);
await Task.WhenAll(legacyRevokeOrgUserTasks);
}
public override async Task<IdentityResult> ConfirmEmailAsync(User user, string token)

View File

@ -288,11 +288,15 @@ public class GlobalSettings : IGlobalSettings
public class AzureServiceBusSettings
{
private string _connectionString;
private string _topicName;
private string _eventTopicName;
private string _integrationTopicName;
public int MaxRetries { get; set; } = 3;
public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription";
public virtual string SlackSubscriptionName { get; set; } = "events-slack-subscription";
public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription";
public virtual string SlackEventSubscriptionName { get; set; } = "events-slack-subscription";
public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription";
public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription";
public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription";
public string ConnectionString
{
@ -300,10 +304,16 @@ public class GlobalSettings : IGlobalSettings
set => _connectionString = value.Trim('"');
}
public string TopicName
public string EventTopicName
{
get => _topicName;
set => _topicName = value.Trim('"');
get => _eventTopicName;
set => _eventTopicName = value.Trim('"');
}
public string IntegrationTopicName
{
get => _integrationTopicName;
set => _integrationTopicName = value.Trim('"');
}
}

View File

@ -712,6 +712,7 @@ public static class CoreHelpers
new(Claims.Premium, isPremium ? "true" : "false"),
new(JwtClaimTypes.Email, user.Email),
new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
// TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant
new(Claims.SecurityStamp, user.SecurityStamp),
};

View File

@ -72,6 +72,10 @@ public class ProfileService : IProfileService
public async Task IsActiveAsync(IsActiveContext context)
{
// We add the security stamp claim to the persisted grant when we issue the refresh token.
// IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that
// was persisted matches the current security stamp of the user. If it does not match, then the user has performed
// an operation that we want to invalidate the refresh token.
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp);
var user = await _userService.GetUserByPrincipalAsync(context.Subject);

View File

@ -199,46 +199,26 @@ public abstract class BaseRequestValidator<T> where T : class
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
/// <summary>
/// Responsible for building the response to the client when the user has successfully authenticated.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
/// <param name="sendRememberToken">Whether to send a 2FA remember token.</param>
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
{
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
var claims = new List<Claim>();
var claims = this.BuildSubjectClaims(user, context, device);
if (device != null)
{
claims.Add(new Claim(Claims.Device, device.Identifier));
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
}
var customResponse = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
customResponse.Add("PrivateKey", user.PrivateKey);
}
if (!string.IsNullOrWhiteSpace(user.Key))
{
customResponse.Add("Key", user.Key);
}
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);
customResponse.Add("KdfParallelism", user.KdfParallelism);
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
if (sendRememberToken)
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
}
var customResponse = await BuildCustomResponse(user, context, device, sendRememberToken);
await ResetFailedAuthDetailsAsync(user);
// Once we've built the claims and custom response, we can set the success result.
// We delegate this to the derived classes, as the implementation varies based on the grant type.
await SetSuccessResult(context, user, claims, customResponse);
}
@ -392,6 +372,71 @@ public abstract class BaseRequestValidator<T> where T : class
return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user));
}
/// <summary>
/// Builds the claims that will be stored on the persisted grant.
/// These claims are supplemented by the claims in the ProfileService when the access token is returned to the client.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
private List<Claim> BuildSubjectClaims(User user, T context, Device device)
{
// We are adding the security stamp claim to the list of claims that will be stored in the persisted grant.
// We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests,
// in the `ProfileService.IsActiveAsync` method.
// If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against.
var claims = new List<Claim>
{
new Claim(Claims.SecurityStamp, user.SecurityStamp)
};
if (device != null)
{
claims.Add(new Claim(Claims.Device, device.Identifier));
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
}
return claims;
}
/// <summary>
/// Builds the custom response that will be sent to the client upon successful authentication, which
/// includes the information needed for the client to initialize the user's account in state.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
/// <param name="sendRememberToken">Whether to send a 2FA remember token.</param>
private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T context, Device device, bool sendRememberToken)
{
var customResponse = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
customResponse.Add("PrivateKey", user.PrivateKey);
}
if (!string.IsNullOrWhiteSpace(user.Key))
{
customResponse.Add("Key", user.Key);
}
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);
customResponse.Add("KdfParallelism", user.KdfParallelism);
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
if (sendRememberToken)
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
}
return customResponse;
}
#nullable enable
/// <summary>
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents

View File

@ -1,5 +1,4 @@
using System.Diagnostics;
using AutoMapper;
using AutoMapper;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums;
@ -446,15 +445,12 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
{
await base.ReplaceAsync(organizationUser);
// Only bump account revision dates for confirmed OrgUsers,
// as this is the only status that receives sync data from the organization
if (organizationUser.Status is not OrganizationUserStatusType.Confirmed)
// Only bump the account revision date if linked to a user account
if (!organizationUser.UserId.HasValue)
{
return;
}
Debug.Assert(organizationUser.UserId is not null, "OrganizationUser is confirmed but does not have a UserId.");
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.Value);

View File

@ -557,7 +557,7 @@ public static class ServiceCollectionExtensions
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
{
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
}
@ -589,86 +589,83 @@ public static class ServiceCollectionExtensions
return services;
}
public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
private static IServiceCollection AddAzureServiceBusEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<AzureTableStorageEventHandler>();
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
services.AddSingleton<IHostedService>(provider =>
new AzureServiceBusEventListenerService(
provider.GetRequiredService<AzureTableStorageEventHandler>(),
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
globalSettings,
globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
services.AddSlackService(globalSettings);
services.AddSingleton<SlackEventHandler>();
services.AddSingleton<IHostedService>(provider =>
new AzureServiceBusEventListenerService(
provider.GetRequiredService<SlackEventHandler>(),
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
globalSettings,
globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
services.AddSingleton<WebhookEventHandler>();
services.AddHttpClient(WebhookEventHandler.HttpClientName);
services.AddSingleton<IHostedService>(provider =>
new AzureServiceBusEventListenerService(
provider.GetRequiredService<WebhookEventHandler>(),
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
globalSettings,
globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName));
}
handler: provider.GetRequiredService<AzureTableStorageEventHandler>(),
logger: provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
globalSettings: globalSettings,
subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
return services;
}
public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
private static IServiceCollection AddAzureServiceBusIntegration<TConfig, THandler>(
this IServiceCollection services,
string eventSubscriptionName,
string integrationSubscriptionName,
IntegrationType integrationType,
GlobalSettings globalSettings)
where TConfig : class
where THandler : class, IIntegrationHandler<TConfig>
{
if (IsRabbitMqEnabled(globalSettings))
var routingKey = integrationType.ToRoutingKey();
services.AddSingleton<IIntegrationPublisher, AzureServiceBusIntegrationPublisher>();
services.AddKeyedSingleton<IEventMessageHandler>(routingKey, (provider, _) =>
new EventIntegrationHandler<TConfig>(
integrationType,
provider.GetRequiredService<IIntegrationPublisher>(),
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
provider.GetRequiredService<IUserRepository>(),
provider.GetRequiredService<IOrganizationRepository>()));
services.AddSingleton<IHostedService>(provider =>
new AzureServiceBusEventListenerService(
handler: provider.GetRequiredKeyedService<IEventMessageHandler>(routingKey),
logger: provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
globalSettings: globalSettings,
subscriptionName: eventSubscriptionName));
services.AddSingleton<IIntegrationHandler<TConfig>, THandler>();
services.AddSingleton<IHostedService>(provider =>
new AzureServiceBusIntegrationListenerService(
handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),
subscriptionName: integrationSubscriptionName,
logger: provider.GetRequiredService<ILogger<AzureServiceBusIntegrationListenerService>>(),
globalSettings: globalSettings));
return services;
}
public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddRabbitMqEventRepositoryListener(globalSettings);
if (!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) ||
!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
return services;
services.AddAzureServiceBusEventRepositoryListener(globalSettings);
services.AddSlackService(globalSettings);
services.AddRabbitMqIntegration<SlackIntegrationConfigurationDetails, SlackIntegrationHandler>(
globalSettings.EventLogging.RabbitMq.SlackEventsQueueName,
globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName,
globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
IntegrationType.Slack,
globalSettings);
services.AddAzureServiceBusIntegration<SlackIntegrationConfigurationDetails, SlackIntegrationHandler>(
eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName,
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName,
integrationType: IntegrationType.Slack,
globalSettings: globalSettings);
services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName,
globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName,
globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
IntegrationType.Webhook,
globalSettings);
}
return services;
}
public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
{
services.AddHttpClient(SlackService.HttpClientName);
services.AddSingleton<ISlackService, SlackService>();
}
else
{
services.AddSingleton<ISlackService, NoopSlackService>();
}
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName,
integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName,
integrationType: IntegrationType.Webhook,
globalSettings: globalSettings);
return services;
}
@ -729,6 +726,36 @@ public static class ServiceCollectionExtensions
return services;
}
public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!IsRabbitMqEnabled(globalSettings))
{
return services;
}
services.AddRabbitMqEventRepositoryListener(globalSettings);
services.AddSlackService(globalSettings);
services.AddRabbitMqIntegration<SlackIntegrationConfigurationDetails, SlackIntegrationHandler>(
globalSettings.EventLogging.RabbitMq.SlackEventsQueueName,
globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName,
globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
IntegrationType.Slack,
globalSettings);
services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName,
globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName,
globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName,
globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
IntegrationType.Webhook,
globalSettings);
return services;
}
private static bool IsRabbitMqEnabled(GlobalSettings settings)
{
return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
@ -737,6 +764,23 @@ public static class ServiceCollectionExtensions
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
}
public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
{
services.AddHttpClient(SlackService.HttpClientName);
services.AddSingleton<ISlackService, SlackService>();
}
else
{
services.AddSingleton<ISlackService, NoopSlackService>();
}
return services;
}
public static void UseDefaultMiddleware(this IApplicationBuilder app,
IWebHostEnvironment env, GlobalSettings globalSettings)
{

View File

@ -36,7 +36,7 @@ BEGIN
END
-- Check if the attachment exists before trying to remove it
IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0
IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL
BEGIN
-- Attachment doesn't exist, nothing to do
RETURN;

View File

@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests
{
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null));
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0));
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);

View File

@ -20,6 +20,7 @@ public class IntegrationMessageTests
message.ApplyRetry(baseline);
Assert.Equal(3, message.RetryCount);
Assert.NotNull(message.DelayUntilDate);
Assert.True(message.DelayUntilDate > baseline);
}

View File

@ -1,5 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
@ -29,7 +32,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers;
public class AcceptOrgUserCommandTests
{
private readonly IUserService _userService = Substitute.For<IUserService>();
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For<IOrgUserInviteTokenableFactory>();
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>();
@ -166,9 +168,6 @@ public class AcceptOrgUserCommandTests
// Arrange
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
// User doesn't have 2FA enabled
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
// Organization they are trying to join requires 2FA
var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId };
sutProvider.GetDependency<IPolicyService>()
@ -185,6 +184,107 @@ public class AcceptOrgUserCommandTests
exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
// Organization they are trying to join requires 2FA
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = orgUser.OrganizationId,
OrganizationUserStatus = OrganizationUserStatusType.Invited,
PolicyType = PolicyType.TwoFactorAuthentication,
}
]));
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService));
Assert.Equal("You cannot join this organization until you enable two-step login on your user account.",
exception.Message);
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
// User has 2FA enabled
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(true);
// Organization they are trying to join requires 2FA
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = orgUser.OrganizationId,
OrganizationUserStatus = OrganizationUserStatusType.Invited,
PolicyType = PolicyType.TwoFactorAuthentication,
}
]));
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
}
[Theory]
[BitAutoData]
public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds(
SutProvider<AcceptOrgUserCommand> sutProvider,
User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails)
{
SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
// Organization they are trying to join doesn't require 2FA
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = Guid.NewGuid(),
OrganizationUserStatus = OrganizationUserStatusType.Invited,
PolicyType = PolicyType.TwoFactorAuthentication,
}
]));
await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<OrganizationUser>(ou => ou.Status == OrganizationUserStatusType.Accepted));
}
[Theory]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
@ -647,9 +747,6 @@ public class AcceptOrgUserCommandTests
.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg)
.Returns(false);
// User doesn't have 2FA enabled
_twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
// Org does not require 2FA
sutProvider.GetDependency<IPolicyService>().GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited)

View File

@ -1,6 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -321,4 +324,122 @@ public class ConfirmOrganizationUserCommandTests
Assert.Contains("User does not have two-step login enabled.", result[1].Item2);
Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2);
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var userRepository = sutProvider.GetDependency<IUserRepository>();
var featureService = sutProvider.GetDependency<IFeatureService>();
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
org.PlanType = PlanType.EnterpriseAnnually;
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
orgUser.UserId = user.Id;
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
organizationRepository.GetByIdAsync(org.Id).Returns(org);
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = org.Id,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id));
Assert.Contains("User does not have two-step login enabled.", exception.Message);
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var userRepository = sutProvider.GetDependency<IUserRepository>();
var featureService = sutProvider.GetDependency<IFeatureService>();
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
org.PlanType = PlanType.EnterpriseAnnually;
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
orgUser.UserId = user.Id;
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
organizationRepository.GetByIdAsync(org.Id).Returns(org);
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = Guid.NewGuid(),
OrganizationUserStatus = OrganizationUserStatusType.Invited,
PolicyType = PolicyType.TwoFactorAuthentication,
}
]));
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) });
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
}
[Theory, BitAutoData]
public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds(
Organization org, OrganizationUser confirmingUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user,
SutProvider<ConfirmOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var userRepository = sutProvider.GetDependency<IUserRepository>();
var featureService = sutProvider.GetDependency<IFeatureService>();
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
org.PlanType = PlanType.EnterpriseAnnually;
orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id;
orgUser.UserId = user.Id;
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser });
organizationRepository.GetByIdAsync(org.Id).Returns(org);
userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user });
featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = org.Id,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(user.Id)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) });
await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await sutProvider.GetDependency<IMailService>().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager);
await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is<List<OrganizationUser>>(users => users.Contains(orgUser) && users.Count == 1));
}
}

View File

@ -1,6 +1,9 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums;
@ -208,6 +211,57 @@ public class RestoreOrganizationUserCommandTests
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organizationUser.OrganizationId,
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
var user = new User();
user.Email = "test@bitwarden.com";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
Organization organization,
@ -235,6 +289,46 @@ public class RestoreOrganizationUserCommandTests
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
[Theory, BitAutoData]
public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organizationUser.OrganizationId,
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
[Theory, BitAutoData]
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
Organization organization,
@ -277,45 +371,6 @@ public class RestoreOrganizationUserCommandTests
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
secondOrganizationUser.UserId = organizationUser.UserId;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
sutProvider.GetDependency<IPolicyService>()
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(true);
var user = new User { Email = "test@bitwarden.com" };
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
Organization organization,
@ -364,20 +419,42 @@ public class RestoreOrganizationUserCommandTests
}
[Theory, BitAutoData]
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null;
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
secondOrganizationUser.UserId = organizationUser.UserId;
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(organizationUser.UserId.Value)
.Returns(new[] { organizationUser, secondOrganizationUser });
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
]);
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
.Returns(new[]
{
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
});
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(organizationUser.UserId.Value)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organizationUser.OrganizationId,
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
var user = new User { Email = "test@bitwarden.com" };
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
@ -385,7 +462,7 @@ public class RestoreOrganizationUserCommandTests
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
await sutProvider.GetDependency<IOrganizationUserRepository>()
.DidNotReceiveWithAnyArgs()
@ -398,35 +475,6 @@ public class RestoreOrganizationUserCommandTests
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
sutProvider.GetDependency<IPolicyService>()
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
]);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
[Theory, BitAutoData]
public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(
Organization organization,
@ -672,6 +720,77 @@ public class RestoreOrganizationUserCommandTests
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
}
[Theory, BitAutoData]
public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
SutProvider<RestoreOrganizationUserCommand> sutProvider)
{
// Arrange
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var userRepository = sutProvider.GetDependency<IUserRepository>();
var policyService = sutProvider.GetDependency<IPolicyService>();
var userService = Substitute.For<IUserService>();
orgUser1.Email = orgUser2.Email = null;
orgUser3.UserId = null;
orgUser3.Key = null;
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
organizationUserRepository
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
// Setup 2FA policy
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(Arg.Any<Guid>())
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organization.Id,
OrganizationUserStatus = OrganizationUserStatusType.Revoked,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
// User1 has 2FA, User2 doesn't
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
{
(orgUser1.UserId!.Value, true),
(orgUser2.UserId!.Value, false)
});
// Act
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
// Assert
Assert.Equal(3, result.Count);
Assert.Empty(result[0].Item2); // First user should succeed
Assert.Contains("two-step login", result[1].Item2); // Second user should fail
Assert.Empty(result[2].Item2); // Third user should succeed
await organizationUserRepository
.Received(1)
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
await organizationUserRepository
.DidNotReceive()
.RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());
await organizationUserRepository
.Received(1)
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
}
[Theory, BitAutoData]
public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,

View File

@ -0,0 +1,117 @@
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
[SutProviderCustomize]
public class RequireTwoFactorPolicyRequirementFactoryTests
{
[Theory]
[BitAutoData]
public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse(
Guid organizationId,
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
}
[Theory]
[BitAutoData]
public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue(
Guid organizationId,
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(
[
new PolicyDetails
{
OrganizationId = organizationId,
PolicyType = PolicyType.TwoFactorAuthentication,
}
]);
Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId));
}
[Theory]
[BitAutoData]
public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse(
Guid organizationId,
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create(
[
new PolicyDetails
{
OrganizationId = Guid.NewGuid(),
PolicyType = PolicyType.TwoFactorAuthentication,
},
]);
Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId));
}
[Theory, BitAutoData]
public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection(
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
{
var actual = sutProvider.Sut.Create([]);
Assert.Empty(actual.OrganizationsRequiringTwoFactor);
}
[Theory, BitAutoData]
public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships(
Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2,
Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4,
SutProvider<RequireTwoFactorPolicyRequirementFactory> sutProvider)
{
var policies = new[]
{
new PolicyDetails
{
OrganizationId = orgId1,
OrganizationUserId = orgUserId1,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserStatus = OrganizationUserStatusType.Accepted
},
new PolicyDetails
{
OrganizationId = orgId2,
OrganizationUserId = orgUserId2,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
},
new PolicyDetails
{
OrganizationId = orgId3,
OrganizationUserId = orgUserId3,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserStatus = OrganizationUserStatusType.Invited
},
new PolicyDetails
{
OrganizationId = orgId4,
OrganizationUserId = orgUserId4,
PolicyType = PolicyType.TwoFactorAuthentication,
OrganizationUserStatus = OrganizationUserStatusType.Revoked
}
};
var actual = sutProvider.Sut.Create(policies);
var result = actual.OrganizationsRequiringTwoFactor.ToList();
Assert.Equal(2, result.Count);
Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1);
Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2);
Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3);
Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4);
}
}

View File

@ -1,219 +0,0 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
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 NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class IntegrationEventHandlerBaseEventHandlerTests
{
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
private const string _templateWithOrganization = "Org: #OrganizationName#";
private const string _templateWithUser = "#UserName#, #UserEmail#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
private const string _url = "https://localhost";
private SutProvider<TestIntegrationEventHandlerBase> GetSutProvider(
List<OrganizationIntegrationConfigurationDetails> configurations)
{
var configurationRepository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
configurationRepository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
return new SutProvider<TestIntegrationEventHandlerBase>()
.SetDependency(configurationRepository)
.Create();
}
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
{
return [];
}
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
config.Template = template;
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations(string template)
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
config.Template = template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
config2.Template = template;
return [config, config2];
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Empty(sutProvider.Sut.CapturedCalls);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Single(sutProvider.Sut.CapturedCalls);
var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}";
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var user = Substitute.For<User>();
user.Email = "test@example.com";
user.Name = "Test";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Single(sutProvider.Sut.CapturedCalls);
var expectedTemplate = $"{user.Name}, {user.Email}";
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
var organization = Substitute.For<Organization>();
organization.Name = "Test";
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Single(sutProvider.Sut.CapturedCalls);
var expectedTemplate = $"Org: {organization.Name}";
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var user = Substitute.For<User>();
user.Email = "test@example.com";
user.Name = "Test";
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Single(sutProvider.Sut.CapturedCalls);
var expectedTemplate = $"{user.Name}, {user.Email}";
Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
Assert.Empty(sutProvider.Sut.CapturedCalls);
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
Assert.Equal(eventMessages.Count, sutProvider.Sut.CapturedCalls.Count);
var index = 0;
foreach (var call in sutProvider.Sut.CapturedCalls)
{
var expected = eventMessages[index];
var expectedTemplate = $"Date: {expected.Date}, Type: {expected.Type}, UserId: {expected.UserId}";
Assert.Equal(expectedTemplate, call.RenderedTemplate);
index++;
}
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes(
List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
Assert.Equal(eventMessages.Count * 2, sutProvider.Sut.CapturedCalls.Count);
var capturedCalls = sutProvider.Sut.CapturedCalls.GetEnumerator();
foreach (var eventMessage in eventMessages)
{
var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}";
Assert.True(capturedCalls.MoveNext());
var call = capturedCalls.Current;
Assert.Equal(expectedTemplate, call.RenderedTemplate);
Assert.True(capturedCalls.MoveNext());
call = capturedCalls.Current;
Assert.Equal(expectedTemplate, call.RenderedTemplate);
}
}
private class TestIntegrationEventHandlerBase : IntegrationEventHandlerBase
{
public TestIntegrationEventHandlerBase(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationIntegrationConfigurationRepository configurationRepository)
: base(userRepository, organizationRepository, configurationRepository)
{ }
public List<(JsonObject MergedConfiguration, string RenderedTemplate)> CapturedCalls { get; } = new();
protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook;
protected override Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate)
{
CapturedCalls.Add((mergedConfiguration, renderedTemplate));
return Task.CompletedTask;
}
}
}

View File

@ -1,181 +0,0 @@
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
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 Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class SlackEventHandlerTests
{
private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
private readonly string _channelId = "C12345";
private readonly string _channelId2 = "C67890";
private readonly string _token = "xoxb-test-token";
private readonly string _token2 = "xoxb-another-test-token";
private SutProvider<SlackEventHandler> GetSutProvider(
List<OrganizationIntegrationConfigurationDetails> integrationConfigurations)
{
_repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
IntegrationType.Slack, Arg.Any<EventType>())
.Returns(integrationConfigurations);
return new SutProvider<SlackEventHandler>()
.SetDependency(_repository)
.SetDependency(_slackService)
.Create();
}
private List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
{
return [];
}
private List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = JsonSerializer.Serialize(new { token = _token });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
return [config];
}
private List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = JsonSerializer.Serialize(new { token = _token });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = JsonSerializer.Serialize(new { token = _token2 });
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 });
config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
return [config, config2];
}
private List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = JsonSerializer.Serialize(new { });
config.IntegrationConfiguration = JsonSerializer.Serialize(new { });
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
return [config];
}
[Theory, BitAutoData]
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleEventAsync(eventMessage);
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
}
[Theory, BitAutoData]
public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration());
await sutProvider.Sut.HandleEventAsync(eventMessage);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(TwoConfigurations());
await sutProvider.Sut.HandleEventAsync(eventMessage);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
Arg.Is(AssertHelper.AssertPropertyEqual(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
);
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
Arg.Is(AssertHelper.AssertPropertyEqual(_token2)),
Arg.Is(AssertHelper.AssertPropertyEqual(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2))
);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(WrongConfiguration());
await sutProvider.Sut.HandleEventAsync(eventMessage);
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(OneConfiguration());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
using var calls = received.GetEnumerator();
Assert.Equal(eventMessages.Count, received.Count());
foreach (var eventMessage in eventMessages)
{
Assert.True(calls.MoveNext());
var arguments = calls.Current.GetArguments();
Assert.Equal(_token, arguments[0] as string);
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
arguments[1] as string);
Assert.Equal(_channelId, arguments[2] as string);
}
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(TwoConfigurations());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
using var calls = received.GetEnumerator();
Assert.Equal(eventMessages.Count * 2, received.Count());
foreach (var eventMessage in eventMessages)
{
Assert.True(calls.MoveNext());
var arguments = calls.Current.GetArguments();
Assert.Equal(_token, arguments[0] as string);
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
arguments[1] as string);
Assert.Equal(_channelId, arguments[2] as string);
Assert.True(calls.MoveNext());
var arguments2 = calls.Current.GetArguments();
Assert.Equal(_token2, arguments2[0] as string);
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
arguments2[1] as string);
Assert.Equal(_channelId2, arguments2[2] as string);
}
}
}

View File

@ -1,235 +0,0 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
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 Bit.Test.Common.Helpers;
using Bit.Test.Common.MockedHttpClient;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class WebhookEventHandlerTests
{
private readonly MockedHttpMessageHandler _handler;
private readonly HttpClient _httpClient;
private const string _template =
"""
{
"Date": "#Date#",
"Type": "#Type#",
"UserId": "#UserId#"
}
""";
private const string _webhookUrl = "http://localhost/test/event";
private const string _webhookUrl2 = "http://localhost/another/event";
public WebhookEventHandlerTests()
{
_handler = new MockedHttpMessageHandler();
_handler.Fallback
.WithStatusCode(HttpStatusCode.OK)
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
_httpClient = _handler.ToHttpClient();
}
private SutProvider<WebhookEventHandler> GetSutProvider(
List<OrganizationIntegrationConfigurationDetails> configurations)
{
var clientFactory = Substitute.For<IHttpClientFactory>();
clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient);
var repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
return new SutProvider<WebhookEventHandler>()
.SetDependency(repository)
.SetDependency(clientFactory)
.Create();
}
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
{
return [];
}
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
config.Template = _template;
return [config];
}
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
config.Template = _template;
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config2.Configuration = null;
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 });
config2.Template = _template;
return [config, config2];
}
private static List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
{
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty });
config.Template = _template;
return [config];
}
[Theory, BitAutoData]
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleEventAsync(eventMessage);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
Assert.Empty(_handler.CapturedRequests);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration());
await sutProvider.Sut.HandleEventAsync(eventMessage);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
Assert.Single(_handler.CapturedRequests);
var request = _handler.CapturedRequests[0];
Assert.NotNull(request);
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
var expected = MockEvent.From(eventMessage);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
AssertHelper.AssertPropertyEqual(expected, returned);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(WrongConfiguration());
await sutProvider.Sut.HandleEventAsync(eventMessage);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
Assert.Empty(_handler.CapturedRequests);
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(NoConfigurations());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
Assert.Empty(_handler.CapturedRequests);
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(OneConfiguration());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count);
var index = 0;
foreach (var request in _handler.CapturedRequests)
{
Assert.NotNull(request);
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
var expected = MockEvent.From(eventMessages[index]);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
AssertHelper.AssertPropertyEqual(expected, returned);
index++;
}
}
[Theory, BitAutoData]
public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List<EventMessage> eventMessages)
{
var sutProvider = GetSutProvider(TwoConfigurations());
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
);
using var capturedRequests = _handler.CapturedRequests.GetEnumerator();
Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count);
foreach (var eventMessage in eventMessages)
{
var expected = MockEvent.From(eventMessage);
Assert.True(capturedRequests.MoveNext());
var request = capturedRequests.Current;
Assert.NotNull(request);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
AssertHelper.AssertPropertyEqual(expected, returned);
Assert.True(capturedRequests.MoveNext());
request = capturedRequests.Current;
Assert.NotNull(request);
Assert.Equal(HttpMethod.Post, request.Method);
Assert.Equal(_webhookUrl2, request.RequestUri.ToString());
returned = await request.Content.ReadFromJsonAsync<MockEvent>();
AssertHelper.AssertPropertyEqual(expected, returned);
}
}
}
public class MockEvent(string date, string type, string userId)
{
public string Date { get; set; } = date;
public string Type { get; set; } = type;
public string UserId { get; set; } = userId;
public static MockEvent From(EventMessage eventMessage)
{
return new MockEvent(
eventMessage.Date.ToString(),
eventMessage.Type.ToString(),
eventMessage.UserId.ToString()
);
}
}

View File

@ -2,8 +2,11 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -326,7 +329,8 @@ public class UserServiceTests
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@ -462,6 +466,78 @@ public class UserServiceTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail(
SutProvider<UserService> sutProvider, User user,
Organization organization1, Guid organizationUserId1,
Organization organization2, Guid organizationUserId2)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true }
});
organization1.Enabled = organization2.Enabled = true;
organization1.UseSso = organization2.UseSso = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organization1.Id,
OrganizationUserId = organizationUserId1,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
},
new PolicyDetails
{
OrganizationId = organization2.Id,
OrganizationUserId = organizationUserId2,
OrganizationUserStatus = OrganizationUserStatusType.Confirmed,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
sutProvider.GetDependency<IOrganizationRepository>()
.GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id)))
.Returns(new[] { organization1, organization2 });
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
// Revoke the user from the first organization
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
r.OrganizationUsers.First().Id == organizationUserId1 &&
r.OrganizationUsers.First().OrganizationId == organization1.Id));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email);
// Remove the user from the second organization
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.Received(1)
.RevokeNonCompliantOrganizationUsersAsync(
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization2.Id &&
r.OrganizationUsers.First().Id == organizationUserId2 &&
r.OrganizationUsers.First().OrganizationId == organization2.Id));
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
SutProvider<UserService> sutProvider, User user, Organization organization)
@ -509,6 +585,53 @@ public class UserServiceTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization(
SutProvider<UserService> sutProvider, User user, Organization organization)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new() { Enabled = true },
[TwoFactorProviderType.Remember] = new() { Enabled = true }
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
.Returns(true);
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id)
.Returns(new RequireTwoFactorPolicyRequirement(
[
new PolicyDetails
{
OrganizationId = organization.Id,
OrganizationUserStatus = OrganizationUserStatusType.Accepted,
PolicyType = PolicyType.TwoFactorAuthentication
}
]));
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user)
.Returns(true);
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Remember] = new() { Enabled = true }
}, JsonHelpers.LegacyEnumKeyResolver);
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
await sutProvider.GetDependency<IUserRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<User>(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
.DidNotReceiveWithAnyArgs()
.RevokeNonCompliantOrganizationUsersAsync(default);
await sutProvider.GetDependency<IMailService>()
.DidNotReceiveWithAnyArgs()
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
@ -800,7 +923,8 @@ public class UserServiceTests
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IPolicyRequirementQuery>()
);
}
}

View File

@ -45,9 +45,6 @@ public class OrganizationUserReplaceTests
/// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other
/// non-Invited statuses (which are all linked to a UserId).
/// </summary>
/// <param name="organizationRepository"></param>
/// <param name="organizationUserRepository"></param>
/// <param name="collectionRepository"></param>
[DatabaseTheory, DatabaseData]
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success(
IUserRepository userRepository,

View File

@ -129,7 +129,7 @@ BEGIN
END
-- Check if the attachment exists before trying to remove it
IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0
IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL
BEGIN
-- Attachment doesn't exist, nothing to do
RETURN;

View File

@ -0,0 +1,78 @@
CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment]
@Id UNIQUEIDENTIFIER,
@AttachmentId VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON
DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"')
DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)
DECLARE @UserId UNIQUEIDENTIFIER
DECLARE @OrganizationId UNIQUEIDENTIFIER
DECLARE @CurrentAttachments NVARCHAR(MAX)
DECLARE @NewAttachments NVARCHAR(MAX)
-- Get current cipher data
SELECT
@UserId = [UserId],
@OrganizationId = [OrganizationId],
@CurrentAttachments = [Attachments]
FROM
[dbo].[Cipher]
WHERE [Id] = @Id
-- If there are no attachments, nothing to do
IF @CurrentAttachments IS NULL
BEGIN
RETURN;
END
-- Validate the initial JSON
IF ISJSON(@CurrentAttachments) = 0
BEGIN
THROW 50000, 'Current initial attachments data is not valid JSON', 1;
RETURN;
END
-- Check if the attachment exists before trying to remove it
IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL
BEGIN
-- Attachment doesn't exist, nothing to do
RETURN;
END
-- Create the new attachments JSON with the specified attachment removed
SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL)
-- Validate the resulting JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when removing attachment', 1;
RETURN;
END
-- Check if we've removed all attachments and have an empty object
IF @NewAttachments = '{}'
BEGIN
-- If we have an empty JSON object, set to NULL instead
SET @NewAttachments = NULL;
END
-- Update with validated JSON
UPDATE [dbo].[Cipher]
SET [Attachments] = @NewAttachments
WHERE [Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_UpdateStorage] @UserId
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO