1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 01:22:50 -05:00

[PM-18527] - Fix allowing restored user to own multiple free orgs (#5444)

* Moved RestoreUserAsync and RestoreUsersAsync to Command.

* Fixing the bug.

* Added test for bulk method.

* Fixing sonar cube warning.

* SonarQube warning fix.

* Excluding org users we already have.

* Fixed misspelling. Added integration test for method.

* test had the misspelling as well 🤦

* Split out interface. Added admin and confirmed constraints.

* fixed queries and added xml comments and tests.
This commit is contained in:
Jared McCannon
2025-03-31 08:33:57 -05:00
committed by GitHub
parent 887332b436
commit 786b0edceb
18 changed files with 1298 additions and 710 deletions

View File

@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
@ -61,6 +62,7 @@ public class OrganizationUsersController : Controller
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
@ -86,7 +88,8 @@ public class OrganizationUsersController : Controller
IPolicyRequirementQuery policyRequirementQuery,
IFeatureService featureService,
IPricingClient pricingClient,
IConfirmOrganizationUserCommand confirmOrganizationUserCommand)
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -112,6 +115,7 @@ public class OrganizationUsersController : Controller
_featureService = featureService;
_pricingClient = pricingClient;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
}
[HttpGet("{id}")]
@ -630,14 +634,14 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}/restore")]
public async Task RestoreAsync(Guid orgId, Guid id)
{
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId));
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
}
[HttpPatch("restore")]
[HttpPut("restore")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _organizationService.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
}
[HttpPatch("enable-secrets-manager")]

View File

@ -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);
}

View File

@ -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");
}
}
}

View File

@ -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);
}

View File

@ -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'.

View File

@ -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;
@ -75,7 +74,6 @@ public class OrganizationService : IOrganizationService
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IOrganizationBillingService _organizationBillingService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
@ -112,7 +110,6 @@ public class OrganizationService : IOrganizationService
IProviderRepository providerRepository,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationBillingService organizationBillingService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient,
IPolicyRequirementQuery policyRequirementQuery)
@ -148,7 +145,6 @@ public class OrganizationService : IOrganizationService
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_organizationBillingService = organizationBillingService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
_policyRequirementQuery = policyRequirementQuery;
@ -1891,144 +1887,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
@ -2095,7 +1953,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;

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
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;
@ -168,6 +169,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>();

View File

@ -196,4 +196,15 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
return result.ToList();
}
}
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{
await using var connection = new SqlConnection(ConnectionString);
return (await connection.QueryAsync<Organization>(
$"[{Schema}].[{Table}_ReadManyByIds]",
new { OrganizationIds = ids.ToGuidIdArrayTVP() },
commandType: CommandType.StoredProcedure))
.ToList();
}
}

View File

@ -354,6 +354,19 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
}
}
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = from organization in dbContext.Organizations
where ids.Contains(organization.Id)
select organization;
return await query.ToArrayAsync();
}
public Task EnableCollectionEnhancements(Guid organizationId)
{
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");

View File

@ -0,0 +1,67 @@
CREATE PROCEDURE [dbo].[Organization_ReadManyByIds] @OrganizationIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
SELECT o.[Id],
o.[Identifier],
o.[Name],
o.[BusinessName],
o.[BusinessAddress1],
o.[BusinessAddress2],
o.[BusinessAddress3],
o.[BusinessCountry],
o.[BusinessTaxNumber],
o.[BillingEmail],
o.[Plan],
o.[PlanType],
o.[Seats],
o.[MaxCollections],
o.[UsePolicies],
o.[UseSso],
o.[UseGroups],
o.[UseDirectory],
o.[UseEvents],
o.[UseTotp],
o.[Use2fa],
o.[UseApi],
o.[UseResetPassword],
o.[SelfHost],
o.[UsersGetPremium],
o.[Storage],
o.[MaxStorageGb],
o.[Gateway],
o.[GatewayCustomerId],
o.[GatewaySubscriptionId],
o.[ReferenceData],
o.[Enabled],
o.[LicenseKey],
o.[PublicKey],
o.[PrivateKey],
o.[TwoFactorProviders],
o.[ExpirationDate],
o.[CreationDate],
o.[RevisionDate],
o.[OwnersNotifiedOfAutoscaling],
o.[MaxAutoscaleSeats],
o.[UseKeyConnector],
o.[UseScim],
o.[UseCustomPermissions],
o.[UseSecretsManager],
o.[Status],
o.[UsePasswordManager],
o.[SmSeats],
o.[SmServiceAccounts],
o.[MaxAutoscaleSmSeats],
o.[MaxAutoscaleSmServiceAccounts],
o.[SecretsManagerBeta],
o.[LimitCollectionCreation],
o.[LimitCollectionDeletion],
o.[LimitItemDeletion],
o.[AllowAdminAccessToAllCollectionItems],
o.[UseRiskInsights]
FROM [dbo].[OrganizationView] o
INNER JOIN @OrganizationIds ids ON o.[Id] = ids.[Id]
END