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:
@ -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")]
|
||||
|
@ -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;
|
||||
@ -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;
|
||||
|
@ -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>();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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.");
|
||||
|
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal file
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal 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
|
||||
|
Reference in New Issue
Block a user