From 6da7fdc39e6076ab7b181c4cb19805d5266258b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:32:29 +0000 Subject: [PATCH 1/2] =?UTF-8?q?[PM-15547]=C2=A0Revoke=20managed=20user=20o?= =?UTF-8?q?n=202FA=20removal=20if=20enforced=20by=20organization=20policy?= =?UTF-8?q?=20(#5124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revoke managed user on 2FA removal if enforced by organization policy * Rename TwoFactorDisabling to TwoFactorDisabled in EventSystemUser enum --- .../AdminConsole/Enums/EventSystemUser.cs | 1 + .../Services/Implementations/UserService.cs | 28 ++- test/Core.Test/Services/UserServiceTests.cs | 171 +++++++++++++++++- 3 files changed, 195 insertions(+), 5 deletions(-) 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/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) { From a28e517eebe28da5f7d89183bbc8af3cc8e00428 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:42:25 +0100 Subject: [PATCH 2/2] [deps] Billing: Update swashbuckle-aspnetcore monorepo to v7 (#5069) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- src/Api/Api.csproj | 2 +- src/SharedWeb/SharedWeb.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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 @@ - +