From dc1319e7fa70c4844bbc70e0b01089b682ac2843 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 29 Jun 2023 13:19:17 +0200 Subject: [PATCH] [PM-1033] refactor(wip): make `AcceptUserCommand` --- .../OrganizationUsersController.cs | 22 ++- .../OrganizationUsers/AcceptUserCommand.cs | 187 ++++++++++++++++++ .../Interfaces/IAcceptUserCommand.cs | 13 ++ src/Core/Services/IOrganizationService.cs | 2 - .../Implementations/OrganizationService.cs | 148 -------------- 5 files changed, 213 insertions(+), 159 deletions(-) create mode 100644 src/Core/OrganizationFeatures/OrganizationUsers/AcceptUserCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationUsers/Interfaces/IAcceptUserCommand.cs diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 28f319235..72f2abb66 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -12,6 +12,7 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -30,6 +31,7 @@ public class OrganizationUsersController : Controller private readonly IPolicyRepository _policyRepository; private readonly ICurrentContext _currentContext; private readonly IUpdateUserResetPasswordEnrollmentCommand _updateUserResetPasswordEnrollmentCommand; + private readonly IAcceptUserCommand _acceptUserCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -40,6 +42,7 @@ public class OrganizationUsersController : Controller IUserService userService, IPolicyRepository policyRepository, IUpdateUserResetPasswordEnrollmentCommand updateUserResetPasswordEnrollmentCommand, + IAcceptUserCommand acceptUserCommand, ICurrentContext currentContext) { _organizationRepository = organizationRepository; @@ -50,6 +53,7 @@ public class OrganizationUsersController : Controller _userService = userService; _policyRepository = policyRepository; _updateUserResetPasswordEnrollmentCommand = updateUserResetPasswordEnrollmentCommand; + _acceptUserCommand = acceptUserCommand; _currentContext = currentContext; } @@ -192,7 +196,7 @@ public class OrganizationUsersController : Controller } await _organizationService.InitPendingOrganization(user.Id, orgId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName); - await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService); + await _acceptUserCommand.AcceptAsync(organizationUserId, user, model.Token); await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService); } @@ -214,7 +218,7 @@ public class OrganizationUsersController : Controller throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided."); } - await _organizationService.AcceptUserAsync(organizationUserId, user, model.Token, _userService); + await _acceptUserCommand.AcceptAsync(organizationUserId, user, model.Token); if (useMasterPasswordPolicy) { @@ -310,7 +314,7 @@ public class OrganizationUsersController : Controller } [HttpPut("{userId}/reset-password-enrollment")] - public async Task PutResetPasswordEnrollment(string orgId, string userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model) + public async Task PutResetPasswordEnrollment(Guid organizationId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -320,13 +324,13 @@ public class OrganizationUsersController : Controller var callingUserId = user.Id; await _updateUserResetPasswordEnrollmentCommand.UpdateAsync( - new Guid(orgId), new Guid(userId), model.ResetPasswordKey, callingUserId); + organizationId, userId, model.ResetPasswordKey, callingUserId); - //if (orgUser.Status == OrganizationUserStatusType.Invited) - //{ - // var user = await _userRepository.GetByIdAsync(userId); - // await _organizationService.AcceptUserAsync(orgUser, user, _userService); - //} + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + if (orgUser.Status == OrganizationUserStatusType.Invited) + { + await _acceptUserCommand.AcceptAsync(organizationId, user); + } } [HttpPut("{id}/reset-password")] diff --git a/src/Core/OrganizationFeatures/OrganizationUsers/AcceptUserCommand.cs b/src/Core/OrganizationFeatures/OrganizationUsers/AcceptUserCommand.cs new file mode 100644 index 000000000..dcb558f06 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationUsers/AcceptUserCommand.cs @@ -0,0 +1,187 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.DataProtection; + +namespace Bit.Core.OrganizationFeatures.OrganizationUsers; + +public class AcceptUserCommand : IAcceptUserCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IDataProtector _dataProtector; + private readonly IMailService _mailService; + private readonly IPolicyService _policyService; + private readonly IGlobalSettings _globalSettings; + private readonly IUserService _userService; + + public AcceptUserCommand( + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IDataProtector dataProtector, + IMailService mailService, + IPolicyService policyService, + IGlobalSettings globalSettings, + IUserService userService) + { + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _dataProtector = dataProtector; + _mailService = mailService; + _policyService = policyService; + _globalSettings = globalSettings; + _userService = userService; + } + + public async Task AcceptAsync(Guid organizationUserId, User user, string token) + { + var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + if (orgUser == null) + { + throw new BadRequestException("User invalid."); + } + + if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings)) + { + throw new BadRequestException("Invalid token."); + } + + var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync( + orgUser.OrganizationId, user.Email, true); + if (existingOrgUserCount > 0) + { + if (orgUser.Status == OrganizationUserStatusType.Accepted) + { + throw new BadRequestException("Invitation already accepted. You will receive an email when your organization membership is confirmed."); + } + throw new BadRequestException("You are already part of this organization."); + } + + if (string.IsNullOrWhiteSpace(orgUser.Email) || + !orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + { + throw new BadRequestException("User email does not match invite."); + } + + return await AcceptAsync(orgUser, user); + } + + public async Task AcceptAsync(string orgIdentifier, User user) + { + var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier); + if (org == null) + { + throw new BadRequestException("Organization invalid."); + } + + var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id); + var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id); + if (orgUser == null) + { + throw new BadRequestException("User not found within organization."); + } + + return await AcceptAsync(orgUser, user); + } + + public async Task AcceptAsync(Guid organizationId, User user) + { + var org = await _organizationRepository.GetByIdAsync(organizationId); + if (org == null) + { + throw new BadRequestException("Organization invalid."); + } + + var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id); + var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id); + if (orgUser == null) + { + throw new BadRequestException("User not found within organization."); + } + + return await AcceptAsync(orgUser, user); + } + + private async Task AcceptAsync(OrganizationUser orgUser, User user) + { + if (orgUser.Status == OrganizationUserStatusType.Revoked) + { + throw new BadRequestException("Your organization access has been revoked."); + } + + if (orgUser.Status != OrganizationUserStatusType.Invited) + { + throw new BadRequestException("Already accepted."); + } + + if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin) + { + var org = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); + if (org.PlanType == PlanType.Free) + { + var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync( + user.Id); + if (adminCount > 0) + { + throw new BadRequestException("You can only be an admin of one free organization."); + } + } + } + + // Enforce Single Organization Policy of organization user is trying to join + var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id); + var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); + var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.SingleOrg, OrganizationUserStatusType.Invited); + + if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) + { + throw new BadRequestException("You may not join this organization until you leave or remove " + + "all other organizations."); + } + + // Enforce Single Organization Policy of other organizations user is a member of + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, + PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You cannot join this organization because you are a member of " + + "another organization which forbids it"); + } + + // Enforce Two Factor Authentication Policy of organization user is trying to join + if (!await _userService.TwoFactorIsEnabledAsync(user)) + { + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable " + + "two-step login on your user account."); + } + } + + orgUser.Status = OrganizationUserStatusType.Accepted; + orgUser.UserId = user.Id; + orgUser.Email = null; + + await _organizationUserRepository.ReplaceAsync(orgUser); + + var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); + var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); + + if (adminEmails.Count > 0) + { + var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); + await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); + } + + return orgUser; + } +} + diff --git a/src/Core/OrganizationFeatures/OrganizationUsers/Interfaces/IAcceptUserCommand.cs b/src/Core/OrganizationFeatures/OrganizationUsers/Interfaces/IAcceptUserCommand.cs new file mode 100644 index 000000000..a9428ee6f --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationUsers/Interfaces/IAcceptUserCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IAcceptUserCommand +{ + Task AcceptAsync(Guid organizationUserId, User user, string token); + + Task AcceptAsync(string orgIdentifier, User user); + + Task AcceptAsync(Guid organizationId, User user); +} + diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index a1b47594e..b7546550b 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -40,8 +40,6 @@ public interface IOrganizationService OrganizationUserType type, bool accessAll, string externalId, IEnumerable collections, IEnumerable groups); Task>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable organizationUsersId); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); - Task AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService); - Task AcceptUserAsync(string orgIdentifier, User user, IUserService userService); Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, IUserService userService); Task>> ConfirmUsersAsync(Guid organizationId, Dictionary keys, diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 0b7681ce9..2ab4ea281 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1213,154 +1213,6 @@ public class OrganizationService : IOrganizationService await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), organization.PlanType == PlanType.Free, initOrganization); } - public async Task AcceptUserAsync(Guid organizationUserId, User user, string token, - IUserService userService) - { - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null) - { - throw new BadRequestException("User invalid."); - } - - if (!CoreHelpers.UserInviteTokenIsValid(_dataProtector, token, user.Email, orgUser.Id, _globalSettings)) - { - throw new BadRequestException("Invalid token."); - } - - var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync( - orgUser.OrganizationId, user.Email, true); - if (existingOrgUserCount > 0) - { - if (orgUser.Status == OrganizationUserStatusType.Accepted) - { - throw new BadRequestException("Invitation already accepted. You will receive an email when your organization membership is confirmed."); - } - throw new BadRequestException("You are already part of this organization."); - } - - if (string.IsNullOrWhiteSpace(orgUser.Email) || - !orgUser.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) - { - throw new BadRequestException("User email does not match invite."); - } - - return await AcceptUserAsync(orgUser, user, userService); - } - - public async Task AcceptUserAsync(string orgIdentifier, User user, IUserService userService) - { - var org = await _organizationRepository.GetByIdentifierAsync(orgIdentifier); - if (org == null) - { - throw new BadRequestException("Organization invalid."); - } - - var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id); - var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id); - if (orgUser == null) - { - throw new BadRequestException("User not found within organization."); - } - - return await AcceptUserAsync(orgUser, user, userService); - } - - public async Task AcceptUserAsync(Guid organizationId, User user, IUserService userService) - { - var org = await _organizationRepository.GetByIdAsync(organizationId); - if (org == null) - { - throw new BadRequestException("Organization invalid."); - } - - var usersOrgs = await _organizationUserRepository.GetManyByUserAsync(user.Id); - var orgUser = usersOrgs.FirstOrDefault(u => u.OrganizationId == org.Id); - if (orgUser == null) - { - throw new BadRequestException("User not found within organization."); - } - - return await AcceptUserAsync(orgUser, user, userService); - } - - private async Task AcceptUserAsync(OrganizationUser orgUser, User user, - IUserService userService) - { - if (orgUser.Status == OrganizationUserStatusType.Revoked) - { - throw new BadRequestException("Your organization access has been revoked."); - } - - if (orgUser.Status != OrganizationUserStatusType.Invited) - { - throw new BadRequestException("Already accepted."); - } - - if (orgUser.Type == OrganizationUserType.Owner || orgUser.Type == OrganizationUserType.Admin) - { - var org = await GetOrgById(orgUser.OrganizationId); - if (org.PlanType == PlanType.Free) - { - var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync( - user.Id); - if (adminCount > 0) - { - throw new BadRequestException("You can only be an admin of one free organization."); - } - } - } - - // Enforce Single Organization Policy of organization user is trying to join - var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(user.Id); - var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var invitedSingleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.SingleOrg, OrganizationUserStatusType.Invited); - - if (hasOtherOrgs && invitedSingleOrgPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You may not join this organization until you leave or remove " + - "all other organizations."); - } - - // Enforce Single Organization Policy of other organizations user is a member of - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(user.Id, - PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - throw new BadRequestException("You cannot join this organization because you are a member of " + - "another organization which forbids it"); - } - - // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await userService.TwoFactorIsEnabledAsync(user)) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot join this organization until you enable " + - "two-step login on your user account."); - } - } - - orgUser.Status = OrganizationUserStatusType.Accepted; - orgUser.UserId = user.Id; - orgUser.Email = null; - - await _organizationUserRepository.ReplaceAsync(orgUser); - - var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(orgUser.OrganizationId, OrganizationUserType.Admin); - var adminEmails = admins.Select(a => a.Email).Distinct().ToList(); - - if (adminEmails.Count > 0) - { - var organization = await _organizationRepository.GetByIdAsync(orgUser.OrganizationId); - await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails); - } - - return orgUser; - } - public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId, IUserService userService) {