mirror of
https://github.com/bitwarden/server.git
synced 2025-07-19 08:30:59 -05:00
Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization
# Conflicts: # src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs # test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs
This commit is contained in:
@ -0,0 +1,54 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
|
||||
/// <summary>
|
||||
/// Restores a user back to their previous status.
|
||||
/// </summary>
|
||||
public interface IRestoreOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||
/// <param name="systemUser">System that is performing the action on behalf of the organization (Public API, SCIM, etc.)</param>
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
/// can re-add this user based on their current occupied seats.
|
||||
///
|
||||
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||
/// other organizations the user may belong to.
|
||||
///
|
||||
/// Reference Events and Push Notifications are fired off for this as well.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">Organization the users should be restored to.</param>
|
||||
/// <param name="organizationUserIds">List of organization user ids to restore to previous status.</param>
|
||||
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||
/// <param name="userService">Passed in from caller to avoid circular dependency</param>
|
||||
/// <returns>List of organization user Ids and strings. A successful restoration will have an empty string.
|
||||
/// If an error occurs, the error message will be provided.</returns>
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
}
|
@ -0,0 +1,295 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
|
||||
public class RestoreOrganizationUserCommand(
|
||||
ICurrentContext currentContext,
|
||||
IEventService eventService,
|
||||
IFeatureService featureService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPolicyService policyService,
|
||||
IUserRepository userRepository,
|
||||
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
|
||||
{
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
{
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!await currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored,
|
||||
systemUser);
|
||||
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
await organizationService.AutoAddSeatsAsync(organization, 1); // Hooray
|
||||
}
|
||||
|
||||
var userTwoFactorIsEnabled = false;
|
||||
// Only check 2FA status if the user is linked to a user account
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
userTwoFactorIsEnabled =
|
||||
(await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([organizationUser.UserId.Value]))
|
||||
.FirstOrDefault()
|
||||
.twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
||||
|
||||
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
|
||||
organizationUser.Status = status;
|
||||
}
|
||||
|
||||
private async Task CheckUserForOtherFreeOrganizationOwnershipAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
var relatedOrgUsersFromOtherOrgs = await organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value);
|
||||
var otherOrgs = await organizationRepository.GetManyByUserIdAsync(organizationUser.UserId.Value);
|
||||
|
||||
var orgOrgUserDict = relatedOrgUsersFromOtherOrgs
|
||||
.Where(x => x.Id != organizationUser.Id)
|
||||
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||
|
||||
CheckForOtherFreeOrganizationOwnership(organizationUser, orgOrgUserDict);
|
||||
}
|
||||
|
||||
private async Task<Dictionary<OrganizationUser, Organization>> GetRelatedOrganizationUsersAndOrganizations(
|
||||
IEnumerable<OrganizationUser> organizationUsers)
|
||||
{
|
||||
var allUserIds = organizationUsers.Select(x => x.UserId.Value);
|
||||
|
||||
var otherOrganizationUsers = (await organizationUserRepository.GetManyByManyUsersAsync(allUserIds))
|
||||
.Where(x => organizationUsers.Any(y => y.Id == x.Id) == false);
|
||||
|
||||
var otherOrgs = await organizationRepository.GetManyByIdsAsync(otherOrganizationUsers
|
||||
.Select(x => x.OrganizationId)
|
||||
.Distinct());
|
||||
|
||||
return otherOrganizationUsers
|
||||
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||
}
|
||||
|
||||
private static void CheckForOtherFreeOrganizationOwnership(OrganizationUser organizationUser,
|
||||
Dictionary<OrganizationUser, Organization> otherOrgUsersAndOrgs)
|
||||
{
|
||||
var ownerOrAdminList = new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||
if (otherOrgUsersAndOrgs.Any(x =>
|
||||
x.Key.UserId == organizationUser.UserId &&
|
||||
ownerOrAdminList.Any(userType => userType == x.Key.Type) &&
|
||||
x.Key.Status == OrganizationUserStatusType.Confirmed &&
|
||||
x.Value.PlanType == PlanType.Free))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||
{
|
||||
var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
.ToList();
|
||||
|
||||
if (filteredUsers.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (restoringUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
// Query Two Factor Authentication status for all users in the organization
|
||||
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||
var organizationUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
||||
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
||||
|
||||
var orgUsersAndOrgs = await GetRelatedOrganizationUsersAndOrganizations(filteredUsers);
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var organizationUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
||||
&& organizationUsersTwoFactorEnabled
|
||||
.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value)
|
||||
.twoFactorIsEnabled;
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
||||
|
||||
CheckForOtherFreeOrganizationOwnership(organizationUser, orgUsersAndOrgs);
|
||||
|
||||
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||
organizationUser.UserId.HasValue)
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(organizationUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
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 (OrganizationService.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 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;
|
||||
}
|
||||
}
|
||||
|
||||
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 policy");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
@ -24,4 +24,5 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
/// </summary>
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||
}
|
||||
|
@ -48,10 +48,6 @@ public interface IOrganizationService
|
||||
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||
/// <summary>
|
||||
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
||||
|
@ -19,7 +19,6 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -71,7 +70,6 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
@ -106,7 +104,6 @@ public class OrganizationService : IOrganizationService
|
||||
IProviderRepository providerRepository,
|
||||
IFeatureService featureService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
@ -140,7 +137,6 @@ public class OrganizationService : IOrganizationService
|
||||
_providerRepository = providerRepository;
|
||||
_featureService = featureService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
@ -1824,144 +1820,6 @@ public class OrganizationService : IOrganizationService
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
{
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||
!await _currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
await AutoAddSeatsAsync(organization, 1);
|
||||
}
|
||||
|
||||
var userTwoFactorIsEnabled = false;
|
||||
// Only check Two Factor Authentication status if the user is linked to a user account
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
||||
|
||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
.ToList();
|
||||
|
||||
if (!filteredUsers.Any())
|
||||
{
|
||||
throw new BadRequestException("Users invalid.");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||
await AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||
|
||||
var deletingUserIsOwner = false;
|
||||
if (restoringUserId.HasValue)
|
||||
{
|
||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
|
||||
// Query Two Factor Authentication status for all users in the organization
|
||||
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
||||
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
|
||||
foreach (var organizationUser in filteredUsers)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
throw new BadRequestException("Already active.");
|
||||
}
|
||||
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
||||
{
|
||||
throw new BadRequestException("You cannot restore yourself.");
|
||||
}
|
||||
|
||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue && !deletingUserIsOwner)
|
||||
{
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
||||
&& organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
|
||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
||||
|
||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
{
|
||||
result.Add(Tuple.Create(organizationUser, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@ -2028,7 +1886,7 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
}
|
||||
|
||||
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||
{
|
||||
// Determine status to revert back to
|
||||
var status = OrganizationUserStatusType.Invited;
|
||||
|
@ -111,12 +111,79 @@ 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";
|
||||
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
||||
public const string NotificationRefresh = "notification-refresh";
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
|
||||
/* Billing Team */
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
|
||||
/* Mobile Team */
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
|
||||
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
|
||||
public const string AndroidMutualTls = "mutual-tls";
|
||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
|
||||
/* Platform Team */
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
public const string WebPush = "web-push";
|
||||
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
||||
|
||||
/* Tools Team */
|
||||
public const string ItemShare = "item-share";
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||
@ -126,70 +193,7 @@ public static class FeatureFlagKeys
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string SecurityTasks = "security-tasks";
|
||||
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||
public const string SSHAgent = "ssh-agent";
|
||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||
public const string Argon2Default = "argon2-default";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||
|
||||
/* Unsorted */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
||||
public const string NotificationRefresh = "notification-refresh";
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
public const string NewDeviceVerification = "new-device-verification";
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string InlineMenuTotp = "inline-menu-totp";
|
||||
public const string AppReviewPrompt = "app-review-prompt";
|
||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||
public const string UsePricingService = "use-pricing-service";
|
||||
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
||||
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
|
||||
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
|
||||
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
|
||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||
public const string AndroidMutualTls = "mutual-tls";
|
||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
||||
public const string WebPush = "web-push";
|
||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -18,6 +18,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
@ -173,6 +174,8 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();
|
||||
services.AddScoped<IGetOrganizationUsersManagementStatusQuery, GetOrganizationUsersManagementStatusQuery>();
|
||||
|
||||
services.AddScoped<IRestoreOrganizationUserCommand, RestoreOrganizationUserCommand>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
|
||||
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
|
||||
|
Reference in New Issue
Block a user