1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-28 13:15:12 +01:00

[PM-1033] refactor(wip): make AcceptUserCommand

This commit is contained in:
Andreas Coroiu 2023-06-29 13:19:17 +02:00
parent 43df689c7f
commit dc1319e7fa
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
5 changed files with 213 additions and 159 deletions

View File

@ -12,6 +12,7 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -30,6 +31,7 @@ public class OrganizationUsersController : Controller
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IUpdateUserResetPasswordEnrollmentCommand _updateUserResetPasswordEnrollmentCommand; private readonly IUpdateUserResetPasswordEnrollmentCommand _updateUserResetPasswordEnrollmentCommand;
private readonly IAcceptUserCommand _acceptUserCommand;
public OrganizationUsersController( public OrganizationUsersController(
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
@ -40,6 +42,7 @@ public class OrganizationUsersController : Controller
IUserService userService, IUserService userService,
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IUpdateUserResetPasswordEnrollmentCommand updateUserResetPasswordEnrollmentCommand, IUpdateUserResetPasswordEnrollmentCommand updateUserResetPasswordEnrollmentCommand,
IAcceptUserCommand acceptUserCommand,
ICurrentContext currentContext) ICurrentContext currentContext)
{ {
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -50,6 +53,7 @@ public class OrganizationUsersController : Controller
_userService = userService; _userService = userService;
_policyRepository = policyRepository; _policyRepository = policyRepository;
_updateUserResetPasswordEnrollmentCommand = updateUserResetPasswordEnrollmentCommand; _updateUserResetPasswordEnrollmentCommand = updateUserResetPasswordEnrollmentCommand;
_acceptUserCommand = acceptUserCommand;
_currentContext = currentContext; _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.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); 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."); 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) if (useMasterPasswordPolicy)
{ {
@ -310,7 +314,7 @@ public class OrganizationUsersController : Controller
} }
[HttpPut("{userId}/reset-password-enrollment")] [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); var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null) if (user == null)
@ -320,13 +324,13 @@ public class OrganizationUsersController : Controller
var callingUserId = user.Id; var callingUserId = user.Id;
await _updateUserResetPasswordEnrollmentCommand.UpdateAsync( await _updateUserResetPasswordEnrollmentCommand.UpdateAsync(
new Guid(orgId), new Guid(userId), model.ResetPasswordKey, callingUserId); organizationId, userId, model.ResetPasswordKey, callingUserId);
//if (orgUser.Status == OrganizationUserStatusType.Invited) var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
//{ if (orgUser.Status == OrganizationUserStatusType.Invited)
// var user = await _userRepository.GetByIdAsync(userId); {
// await _organizationService.AcceptUserAsync(orgUser, user, _userService); await _acceptUserCommand.AcceptAsync(organizationId, user);
//} }
} }
[HttpPut("{id}/reset-password")] [HttpPut("{id}/reset-password")]

View File

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

View File

@ -0,0 +1,13 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IAcceptUserCommand
{
Task<OrganizationUser> AcceptAsync(Guid organizationUserId, User user, string token);
Task<OrganizationUser> AcceptAsync(string orgIdentifier, User user);
Task<OrganizationUser> AcceptAsync(Guid organizationId, User user);
}

View File

@ -40,8 +40,6 @@ public interface IOrganizationService
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups); OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections, IEnumerable<Guid> groups);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId); Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false); Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token, IUserService userService);
Task<OrganizationUser> AcceptUserAsync(string orgIdentifier, User user, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService); Guid confirmingUserId, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys, Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,

View File

@ -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); await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5)), organization.PlanType == PlanType.Free, initOrganization);
} }
public async Task<OrganizationUser> 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<OrganizationUser> 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<OrganizationUser> 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<OrganizationUser> 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<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService) Guid confirmingUserId, IUserService userService)
{ {