mirror of
https://github.com/bitwarden/server.git
synced 2025-04-16 02:28:13 -05:00

* Add ConfirmOrganizationUserCommand and IConfirmOrganizationUserCommand interface for managing organization user confirmations * Add unit tests for ConfirmOrganizationUserCommand to validate user confirmation scenarios * Register ConfirmOrganizationUserCommand for dependency injection * Refactor OrganizationUsersController to utilize IConfirmOrganizationUserCommand for user confirmation processes * Remove ConfirmUserAsync and ConfirmUsersAsync methods from IOrganizationService and OrganizationService * Rename test methods in ConfirmOrganizationUserCommandTests for clarity and consistency * Update test method name in ConfirmOrganizationUserCommandTests for improved clarity
187 lines
8.4 KiB
C#
187 lines
8.4 KiB
C#
using Bit.Core.AdminConsole.Enums;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
|
using Bit.Core.AdminConsole.Services;
|
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
|
using Bit.Core.Billing.Enums;
|
|
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;
|
|
|
|
public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
|
|
{
|
|
private readonly IOrganizationRepository _organizationRepository;
|
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly IEventService _eventService;
|
|
private readonly IMailService _mailService;
|
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
|
private readonly IPushNotificationService _pushNotificationService;
|
|
private readonly IPushRegistrationService _pushRegistrationService;
|
|
private readonly IPolicyService _policyService;
|
|
private readonly IDeviceRepository _deviceRepository;
|
|
|
|
public ConfirmOrganizationUserCommand(
|
|
IOrganizationRepository organizationRepository,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
IUserRepository userRepository,
|
|
IEventService eventService,
|
|
IMailService mailService,
|
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
|
IPushNotificationService pushNotificationService,
|
|
IPushRegistrationService pushRegistrationService,
|
|
IPolicyService policyService,
|
|
IDeviceRepository deviceRepository)
|
|
{
|
|
_organizationRepository = organizationRepository;
|
|
_organizationUserRepository = organizationUserRepository;
|
|
_userRepository = userRepository;
|
|
_eventService = eventService;
|
|
_mailService = mailService;
|
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
_pushNotificationService = pushNotificationService;
|
|
_pushRegistrationService = pushRegistrationService;
|
|
_policyService = policyService;
|
|
_deviceRepository = deviceRepository;
|
|
}
|
|
|
|
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
|
|
Guid confirmingUserId)
|
|
{
|
|
var result = await ConfirmUsersAsync(
|
|
organizationId,
|
|
new Dictionary<Guid, string>() { { organizationUserId, key } },
|
|
confirmingUserId);
|
|
|
|
if (!result.Any())
|
|
{
|
|
throw new BadRequestException("User not valid.");
|
|
}
|
|
|
|
var (orgUser, error) = result[0];
|
|
if (error != "")
|
|
{
|
|
throw new BadRequestException(error);
|
|
}
|
|
return orgUser;
|
|
}
|
|
|
|
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
|
|
Guid confirmingUserId)
|
|
{
|
|
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
|
|
var validSelectedOrganizationUsers = selectedOrganizationUsers
|
|
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
|
|
.ToList();
|
|
|
|
if (!validSelectedOrganizationUsers.Any())
|
|
{
|
|
return new List<Tuple<OrganizationUser, string>>();
|
|
}
|
|
|
|
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
|
|
|
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
|
|
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
|
|
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
|
|
|
|
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
|
|
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
|
|
.ToDictionary(u => u.Key, u => u.ToList());
|
|
|
|
var succeededUsers = new List<OrganizationUser>();
|
|
var result = new List<Tuple<OrganizationUser, string>>();
|
|
|
|
foreach (var user in users)
|
|
{
|
|
if (!keyedFilteredUsers.ContainsKey(user.Id))
|
|
{
|
|
continue;
|
|
}
|
|
var orgUser = keyedFilteredUsers[user.Id];
|
|
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
|
|
try
|
|
{
|
|
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|
|
|| orgUser.Type == OrganizationUserType.Owner))
|
|
{
|
|
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
|
|
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
|
|
if (adminCount > 0)
|
|
{
|
|
throw new BadRequestException("User can only be an admin of one free organization.");
|
|
}
|
|
}
|
|
|
|
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
|
|
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
|
|
orgUser.Status = OrganizationUserStatusType.Confirmed;
|
|
orgUser.Key = keys[orgUser.Id];
|
|
orgUser.Email = null;
|
|
|
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
|
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
|
|
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
|
succeededUsers.Add(orgUser);
|
|
result.Add(Tuple.Create(orgUser, ""));
|
|
}
|
|
catch (BadRequestException e)
|
|
{
|
|
result.Add(Tuple.Create(orgUser, e.Message));
|
|
}
|
|
}
|
|
|
|
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task CheckPoliciesAsync(Guid organizationId, User user,
|
|
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
|
|
{
|
|
// Enforce Two Factor Authentication Policy for this organization
|
|
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication))
|
|
.Any(p => p.OrganizationId == organizationId);
|
|
if (orgRequiresTwoFactor && !twoFactorEnabled)
|
|
{
|
|
throw new BadRequestException("User does not have two-step login enabled.");
|
|
}
|
|
|
|
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
|
|
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
|
|
var otherSingleOrgPolicies =
|
|
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
|
|
// Enforce Single Organization Policy for this organization
|
|
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
|
|
{
|
|
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
|
|
}
|
|
// Enforce Single Organization Policy of other organizations user is a member of
|
|
if (otherSingleOrgPolicies.Any())
|
|
{
|
|
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
|
}
|
|
}
|
|
|
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
|
{
|
|
var devices = await GetUserDeviceIdsAsync(userId);
|
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
|
|
organizationId.ToString());
|
|
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
|
|
}
|
|
|
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
|
{
|
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
|
return devices
|
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
|
.Select(d => d.Id.ToString());
|
|
}
|
|
}
|