using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; 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.Tokens; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; namespace Bit.Core.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommand : IAcceptOrgUserCommand { private readonly IDataProtector _dataProtector; private readonly IGlobalSettings _globalSettings; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IPolicyService _policyService; private readonly IMailService _mailService; private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, IGlobalSettings globalSettings, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IPolicyService policyService, IMailService mailService, IUserRepository userRepository, IDataProtectorTokenFactory orgUserInviteTokenDataFactory) { // TODO: remove data protector when old token validation removed _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); _globalSettings = globalSettings; _organizationUserRepository = organizationUserRepository; _organizationRepository = organizationRepository; _policyService = policyService; _mailService = mailService; _userRepository = userRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; } public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, IUserService userService) { var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); if (orgUser == null) { throw new BadRequestException("User invalid."); } // Tokens will have been created in two ways in the OrganizationService invite methods: // 1. New way - via OrgUserInviteTokenable // 2. Old way - via manual process using data protector initialized with purpose: "OrganizationServiceDataProtector" // For backwards compatibility, must check validity of both types of tokens and accept if either is valid // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( _orgUserInviteTokenDataFactory, emailToken, orgUser); var tokenValid = newTokenValid || CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id, _globalSettings); if (!tokenValid) { 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."); } var organizationUser = await AcceptOrgUserAsync(orgUser, user, userService); // Verify user email if they accept org invite via email link if (user.EmailVerified == false) { user.EmailVerified = true; await _userRepository.ReplaceAsync(user); } return organizationUser; } private bool ValidateOrgUserInviteToken(string orgUserInviteToken, OrganizationUser orgUser) { return _orgUserInviteTokenDataFactory.TryUnprotect(orgUserInviteToken, out var decryptedToken) && decryptedToken.Valid && decryptedToken.TokenIsValid(orgUser); } public async Task AcceptOrgUserByOrgSsoIdAsync(string orgSsoIdentifier, User user, IUserService userService) { var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier); if (org == null) { throw new BadRequestException("Organization invalid."); } var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id); if (orgUser == null) { throw new BadRequestException("User not found within organization."); } return await AcceptOrgUserAsync(orgUser, user, userService); } public async Task AcceptOrgUserByOrgIdAsync(Guid organizationId, User user, IUserService userService) { var org = await _organizationRepository.GetByIdAsync(organizationId); if (org == null) { throw new BadRequestException("Organization invalid."); } var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, user.Id); if (orgUser == null) { throw new BadRequestException("User not found within organization."); } return await AcceptOrgUserAsync(orgUser, user, userService); } public async Task AcceptOrgUserAsync(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 _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; } }