mirror of
https://github.com/bitwarden/server.git
synced 2025-04-12 00:28:11 -05:00
[PM-15547] Revoke managed user on 2FA removal if enforced by organization policy (#5124)
* Revoke managed user on 2FA removal if enforced by organization policy * Rename TwoFactorDisabling to TwoFactorDisabled in EventSystemUser enum
This commit is contained in:
parent
6d9c8d0a47
commit
6da7fdc39e
@ -6,4 +6,5 @@ public enum EventSystemUser : byte
|
|||||||
SCIM = 1,
|
SCIM = 1,
|
||||||
DomainVerification = 2,
|
DomainVerification = 2,
|
||||||
PublicApi = 3,
|
PublicApi = 3,
|
||||||
|
TwoFactorDisabled = 4,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
@ -14,6 +16,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -67,6 +70,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
|
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
@ -101,7 +105,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IPremiumUserBillingService premiumUserBillingService,
|
IPremiumUserBillingService premiumUserBillingService,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
|
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||||
: base(
|
: base(
|
||||||
store,
|
store,
|
||||||
optionsAccessor,
|
optionsAccessor,
|
||||||
@ -142,6 +147,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_premiumUserBillingService = premiumUserBillingService;
|
_premiumUserBillingService = premiumUserBillingService;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
|
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
public Guid? GetProperUserId(ClaimsPrincipal principal)
|
||||||
@ -1355,13 +1361,27 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
|
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
|
||||||
{
|
{
|
||||||
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
|
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
|
||||||
|
var organizationsManagingUser = await GetOrganizationsManagingUserAsync(user.Id);
|
||||||
|
|
||||||
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
|
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
|
||||||
{
|
{
|
||||||
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && organizationsManagingUser.Any(o => o.Id == p.OrganizationId))
|
||||||
organization.DisplayName(), user.Email);
|
{
|
||||||
|
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();
|
}).ToArray();
|
||||||
|
|
||||||
await Task.WhenAll(removeOrgUserTasks);
|
await Task.WhenAll(removeOrgUserTasks);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
@ -10,13 +12,16 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -268,7 +273,8 @@ public class UserServiceTests
|
|||||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
||||||
sutProvider.GetDependency<IFeatureService>(),
|
sutProvider.GetDependency<IFeatureService>(),
|
||||||
sutProvider.GetDependency<IPremiumUserBillingService>(),
|
sutProvider.GetDependency<IPremiumUserBillingService>(),
|
||||||
sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
|
||||||
|
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||||
);
|
);
|
||||||
|
|
||||||
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
||||||
@ -353,6 +359,169 @@ public class UserServiceTests
|
|||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail(
|
||||||
|
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
|
{
|
||||||
|
[TwoFactorProviderType.Email] = new() { Enabled = true }
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
|
||||||
|
.Returns(
|
||||||
|
[
|
||||||
|
new OrganizationUserPolicyDetails
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||||
|
PolicyEnabled = true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
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);
|
||||||
|
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.RemoveUserAsync(organization.Id, user.Id);
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.Received(1)
|
||||||
|
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization(
|
||||||
|
SutProvider<UserService> sutProvider, User user, Organization organization)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
|
{
|
||||||
|
[TwoFactorProviderType.Email] = new() { Enabled = true },
|
||||||
|
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||||
|
});
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)
|
||||||
|
.Returns(
|
||||||
|
[
|
||||||
|
new OrganizationUserPolicyDetails
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
PolicyType = PolicyType.TwoFactorAuthentication,
|
||||||
|
PolicyEnabled = true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||||
|
{
|
||||||
|
[TwoFactorProviderType.Remember] = new() { Enabled = true }
|
||||||
|
}, JsonHelpers.LegacyEnumKeyResolver);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
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);
|
||||||
|
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RemoveUserAsync(default, default);
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_WhenUserIsManaged_DisablingAllProviders_RemovesOrRevokesUserAndSendsEmail(
|
||||||
|
SutProvider<UserService> sutProvider, User user, Organization organization1, Organization organization2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
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.AccountDeprovisioning)
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.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<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization1.Id)
|
||||||
|
.Returns(organization1);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization2.Id)
|
||||||
|
.Returns(organization2);
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByVerifiedUserEmailDomainAsync(user.Id)
|
||||||
|
.Returns(new[] { organization1 });
|
||||||
|
var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary<TwoFactorProviderType, TwoFactorProvider>(), JsonHelpers.LegacyEnumKeyResolver);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
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 because they are managed by it
|
||||||
|
await sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.RevokeNonCompliantOrganizationUsersAsync(
|
||||||
|
Arg.Is<RevokeOrganizationUsersRequest>(r => r.OrganizationId == organization1.Id &&
|
||||||
|
r.OrganizationUsers.First().UserId == user.Id &&
|
||||||
|
r.OrganizationUsers.First().OrganizationId == organization1.Id));
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.Received(1)
|
||||||
|
.SendOrganizationUserRevokedForTwoFactoryPolicyEmailAsync(organization1.DisplayName(), user.Email);
|
||||||
|
|
||||||
|
// Remove the user from the second organization because they are not managed by it
|
||||||
|
await sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.RemoveUserAsync(organization2.Id, user.Id);
|
||||||
|
await sutProvider.GetDependency<IMailService>()
|
||||||
|
.Received(1)
|
||||||
|
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email);
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetupUserAndDevice(User user,
|
private static void SetupUserAndDevice(User user,
|
||||||
bool shouldHavePassword)
|
bool shouldHavePassword)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user