mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 20:11:04 -05:00
Update RestoreOrganizationUserCommand to use IPolicyRequirementQuery for two-factor authentication policies checks
This commit is contained in:
parent
30490ca383
commit
a0ffcc81cb
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
@ -22,7 +23,9 @@ public class RestoreOrganizationUserCommand(
|
|||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
|
IOrganizationService organizationService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand
|
||||||
{
|
{
|
||||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||||
{
|
{
|
||||||
@ -270,11 +273,19 @@ public class RestoreOrganizationUserCommand(
|
|||||||
// Enforce 2FA Policy of organization user is trying to join
|
// Enforce 2FA Policy of organization user is trying to join
|
||||||
if (!userHasTwoFactorEnabled)
|
if (!userHasTwoFactorEnabled)
|
||||||
{
|
{
|
||||||
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
|
||||||
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
|
||||||
{
|
{
|
||||||
twoFactorCompliant = false;
|
var requirement = await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(userId);
|
||||||
|
twoFactorCompliant = requirement.CanBeRestored(userHasTwoFactorEnabled, orgUser.OrganizationId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
||||||
|
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||||
|
{
|
||||||
|
twoFactorCompliant = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
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.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
@ -208,6 +210,57 @@ public class RestoreOrganizationUserCommandTests
|
|||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
.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]
|
[Theory, BitAutoData]
|
||||||
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -235,6 +288,46 @@ public class RestoreOrganizationUserCommandTests
|
|||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
.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]
|
[Theory, BitAutoData]
|
||||||
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -363,6 +456,63 @@ public class RestoreOrganizationUserCommandTests
|
|||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
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; // 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.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);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
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()
|
||||||
|
.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]
|
[Theory, BitAutoData]
|
||||||
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -672,6 +822,77 @@ public class RestoreOrganizationUserCommandTests
|
|||||||
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
|
.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]
|
[Theory, BitAutoData]
|
||||||
public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
|
public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user