diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index d56bb2796f9b..f42f22615307 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
- "version": "6.9.0",
+ "version": "7.2.0",
"commands": ["swagger"]
},
"dotnet-ef": {
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index 4a018b2198a0..c490e90150d1 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/Core/AdminConsole/Enums/EventSystemUser.cs b/src/Core/AdminConsole/Enums/EventSystemUser.cs
index c3e13705dd9f..1eb1e5b4abb3 100644
--- a/src/Core/AdminConsole/Enums/EventSystemUser.cs
+++ b/src/Core/AdminConsole/Enums/EventSystemUser.cs
@@ -6,4 +6,5 @@ public enum EventSystemUser : byte
SCIM = 1,
DomainVerification = 2,
PublicApi = 3,
+ TwoFactorDisabled = 4,
}
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index fa8cd3cef855..2bc81959b9b3 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -1,7 +1,9 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
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.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@@ -14,6 +16,7 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
+using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@@ -67,6 +70,7 @@ public class UserService : UserManager, IUserService, IDisposable
private readonly IFeatureService _featureService;
private readonly IPremiumUserBillingService _premiumUserBillingService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
+ private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
public UserService(
IUserRepository userRepository,
@@ -101,7 +105,8 @@ public UserService(
IDataProtectorTokenFactory orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPremiumUserBillingService premiumUserBillingService,
- IRemoveOrganizationUserCommand removeOrganizationUserCommand)
+ IRemoveOrganizationUserCommand removeOrganizationUserCommand,
+ IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
: base(
store,
optionsAccessor,
@@ -142,6 +147,7 @@ public UserService(
_featureService = featureService;
_premiumUserBillingService = premiumUserBillingService;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
+ _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@@ -1355,13 +1361,27 @@ public void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool set
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
+ var organizationsManagingUser = await GetOrganizationsManagingUserAsync(user.Id);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
- await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
- await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
- organization.DisplayName(), user.Email);
+ if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId))
+ {
+ await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
+ new RevokeOrganizationUsersRequest(
+ p.OrganizationId,
+ [new OrganizationUserUserDetails { UserId = user.Id, OrganizationId = p.OrganizationId }],
+ new SystemUser(EventSystemUser.TwoFactorDisabled)));
+ await _mailService.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization.DisplayName(), user.Email);
+ }
+ else
+ {
+ await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
+ await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
+ organization.DisplayName(), user.Email);
+ }
+
}).ToArray();
await Task.WhenAll(removeOrgUserTasks);
diff --git a/src/SharedWeb/SharedWeb.csproj b/src/SharedWeb/SharedWeb.csproj
index 8d1097eeec5f..6df65b2310f2 100644
--- a/src/SharedWeb/SharedWeb.csproj
+++ b/src/SharedWeb/SharedWeb.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs
index 71cceb86ad08..de2a5187171c 100644
--- a/test/Core.Test/Services/UserServiceTests.cs
+++ b/test/Core.Test/Services/UserServiceTests.cs
@@ -1,7 +1,9 @@
using System.Security.Claims;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@@ -10,13 +12,16 @@
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
+using Bit.Core.Enums;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
+using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Services;
+using Bit.Core.Utilities;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -268,7 +273,8 @@ public async Task VerifySecretAsync_Works(
new FakeDataProtectorTokenFactory(),
sutProvider.GetDependency(),
sutProvider.GetDependency(),
- sutProvider.GetDependency()
+ sutProvider.GetDependency(),
+ sutProvider.GetDependency()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@@ -353,6 +359,169 @@ public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabl
Assert.False(result);
}
+ [Theory, BitAutoData]
+ public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail(
+ SutProvider sutProvider, User user, Organization organization)
+ {
+ // Arrange
+ user.SetTwoFactorProviders(new Dictionary
+ {
+ [TwoFactorProviderType.Email] = new() { Enabled = true }
+ });
+ sutProvider.GetDependency()
+ .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
+ .Returns(
+ [
+ new OrganizationUserPolicyDetails
+ {
+ OrganizationId = organization.Id,
+ PolicyType = PolicyType.TwoFactorAuthentication,
+ PolicyEnabled = true
+ }
+ ]);
+ sutProvider.GetDependency()
+ .GetByIdAsync(organization.Id)
+ .Returns(organization);
+ var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver);
+
+ // Act
+ await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
+ await sutProvider.GetDependency()
+ .Received(1)
+ .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
+ await sutProvider.GetDependency()
+ .Received(1)
+ .RemoveUserAsync(organization.Id, user.Id);
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email);
+ }
+
+ [Theory, BitAutoData]
+ public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization(
+ SutProvider sutProvider, User user, Organization organization)
+ {
+ // Arrange
+ user.SetTwoFactorProviders(new Dictionary
+ {
+ [TwoFactorProviderType.Email] = new() { Enabled = true },
+ [TwoFactorProviderType.Remember] = new() { Enabled = true }
+ });
+ sutProvider.GetDependency()
+ .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
+ .Returns(
+ [
+ new OrganizationUserPolicyDetails
+ {
+ OrganizationId = organization.Id,
+ PolicyType = PolicyType.TwoFactorAuthentication,
+ PolicyEnabled = true
+ }
+ ]);
+ sutProvider.GetDependency()
+ .GetByIdAsync(organization.Id)
+ .Returns(organization);
+ var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary
+ {
+ [TwoFactorProviderType.Remember] = new() { Enabled = true }
+ }, JsonHelpers.LegacyEnumKeyResolver);
+
+ // Act
+ await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
+ await sutProvider.GetDependency()
+ .Received(1)
+ .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
+ await sutProvider.GetDependency()
+ .DidNotReceiveWithAnyArgs()
+ .RemoveUserAsync(default, default);
+ await sutProvider.GetDependency()
+ .DidNotReceiveWithAnyArgs()
+ .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
+ }
+
+ [Theory, BitAutoData]
+ public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_WhenUserIsManaged_DisablingAllProviders_RemovesOrRevokesUserAndSendsEmail(
+ SutProvider sutProvider, User user, Organization organization1, Organization organization2)
+ {
+ // Arrange
+ user.SetTwoFactorProviders(new Dictionary
+ {
+ [TwoFactorProviderType.Email] = new() { Enabled = true }
+ });
+ organization1.Enabled = organization2.Enabled = true;
+ organization1.UseSso = organization2.UseSso = true;
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
+ .Returns(true);
+ sutProvider.GetDependency()
+ .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
+ .Returns(
+ [
+ new OrganizationUserPolicyDetails
+ {
+ OrganizationId = organization1.Id,
+ PolicyType = PolicyType.TwoFactorAuthentication,
+ PolicyEnabled = true
+ },
+ new OrganizationUserPolicyDetails
+ {
+ OrganizationId = organization2.Id,
+ PolicyType = PolicyType.TwoFactorAuthentication,
+ PolicyEnabled = true
+ }
+ ]);
+ sutProvider.GetDependency()
+ .GetByIdAsync(organization1.Id)
+ .Returns(organization1);
+ sutProvider.GetDependency()
+ .GetByIdAsync(organization2.Id)
+ .Returns(organization2);
+ sutProvider.GetDependency()
+ .GetByVerifiedUserEmailDomainAsync(user.Id)
+ .Returns(new[] { organization1 });
+ var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver);
+
+ // Act
+ await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
+
+ // Assert
+ await sutProvider.GetDependency()
+ .Received(1)
+ .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders));
+ await sutProvider.GetDependency()
+ .Received(1)
+ .LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
+
+ // Revoke the user from the first organization because they are managed by it
+ await sutProvider.GetDependency()
+ .Received(1)
+ .RevokeNonCompliantOrganizationUsersAsync(
+ Arg.Is(r => r.OrganizationId == organization1.Id &&
+ r.OrganizationUsers.First().UserId == user.Id &&
+ r.OrganizationUsers.First().OrganizationId == organization1.Id));
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization1.DisplayName(), user.Email);
+
+ // Remove the user from the second organization because they are not managed by it
+ await sutProvider.GetDependency()
+ .Received(1)
+ .RemoveUserAsync(organization2.Id, user.Id);
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email);
+ }
+
private static void SetupUserAndDevice(User user,
bool shouldHavePassword)
{