mirror of
https://github.com/bitwarden/server.git
synced 2025-06-13 14:30:50 -05:00

* Add RequireTwoFactorPolicyRequirement and its factory with unit tests * Implemented RequireTwoFactorPolicyRequirement to enforce two-factor authentication policies. * Created RequireTwoFactorPolicyRequirementFactory to generate policy requirements based on user status. * Added unit tests for the factory to validate behavior with various user statuses and policy details. * Enhance AcceptOrgUserCommand to use IPolicyRequirementQuery for two-factor authentication validation * Update ConfirmOrganizationUserCommand to use RequireTwoFactorPolicyRequirement to check for 2FA requirement * Implement CanAcceptInvitation and CanBeConfirmed methods in RequireTwoFactorPolicyRequirement; update tests to reflect new logic for two-factor authentication policy handling. * Refactor AcceptOrgUserCommand to enforce two-factor authentication policy based on feature flag; update validation logic and tests accordingly. * Enhance ConfirmOrganizationUserCommand to validate two-factor authentication policy based on feature flag; refactor validation logic and update related tests for improved policy handling. * Remove unused method and its dependencies from OrganizationService. * Implement CanBeRestored method in RequireTwoFactorPolicyRequirement to determine user restoration eligibility based on two-factor authentication status; add corresponding unit tests for various scenarios. * Update RestoreOrganizationUserCommand to use IPolicyRequirementQuery for two-factor authentication policies checks * Remove redundant vNext tests * Add TwoFactorPoliciesForActiveMemberships property to RequireTwoFactorPolicyRequirement and corresponding unit tests for policy retrieval based on user status * Refactor UserService to integrate IPolicyRequirementQuery for two-factor authentication policy checks * Add XML documentation for TwoFactorPoliciesForActiveMemberships property in RequireTwoFactorPolicyRequirement to clarify its purpose and return value. * Add exception documentation for ValidateTwoFactorAuthenticationPolicyAsync method in ConfirmOrganizationUserCommand to clarify error handling for users without two-step login enabled. * Update comments in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to clarify handling of two-step login and 2FA policy checks. * Add RequireTwoFactorPolicyRequirementFactory to PolicyServiceCollectionExtensions * Refactor two-factor authentication policy checks in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to streamline validation logic and improve clarity. Update RequireTwoFactorPolicyRequirement to provide a method for checking if two-factor authentication is required for an organization. Adjust related unit tests accordingly. * Add PolicyRequirements namespace * Update comments in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to clarify two-factor authentication policy requirements and exception handling. * Refactor RequireTwoFactorPolicyRequirement to return tuples of (OrganizationId, OrganizationUserId) for active memberships requiring two-factor authentication. Update UserService and related tests to reflect this change. * Refactor AcceptOrgUserCommand: delegate feature flag check to the ValidateTwoFactorAuthenticationPolicyAsync method * Skip policy check if two-step login is enabled for the user * Refactor ConfirmOrganizationUserCommand to streamline two-factor authentication policy validation logic * Refactor AcceptOrgUserCommand to simplify two-factor authentication check by removing intermediate variable * Update documentation in RequireTwoFactorPolicyRequirement to clarify the purpose of the IsTwoFactorRequiredForOrganization * Refactor AcceptOrgUserCommandTests to remove redundant two-factor authentication checks and simplify test setup * Refactor AcceptOrgUserCommand and ConfirmOrganizationUserCommand to streamline two-factor authentication checks by removing redundant conditions and simplifying logic flow. * Rename removeOrgUserTasks variable in UserService * Refactor RestoreOrganizationUserCommand to simplify two-factor authentication compliance checks by consolidating logic into a new method, IsTwoFactorRequiredForOrganizationAsync. * Remove outdated two-factor authentication validation documentation from AcceptOrgUserCommand * Invert two-factor compliance check in RestoreOrganizationUserCommand to ensure correct validation of organization user policies. * Refactor UserService to enhance two-factor compliance checks by optimizing organization retrieval and logging when no organizations require two-factor authentication.
256 lines
11 KiB
C#
256 lines
11 KiB
C#
using Bit.Core.AdminConsole.Enums;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
|
using Bit.Core.AdminConsole.Services;
|
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
|
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.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 ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
|
private readonly IFeatureService _featureService;
|
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
|
|
|
public AcceptOrgUserCommand(
|
|
IDataProtectionProvider dataProtectionProvider,
|
|
IGlobalSettings globalSettings,
|
|
IOrganizationUserRepository organizationUserRepository,
|
|
IOrganizationRepository organizationRepository,
|
|
IPolicyService policyService,
|
|
IMailService mailService,
|
|
IUserRepository userRepository,
|
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
|
IFeatureService featureService,
|
|
IPolicyRequirementQuery policyRequirementQuery)
|
|
{
|
|
// 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;
|
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
|
_featureService = featureService;
|
|
_policyRequirementQuery = policyRequirementQuery;
|
|
}
|
|
|
|
public async Task<OrganizationUser> 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<OrganizationUser> 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<OrganizationUser> 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<OrganizationUser> 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
|
|
await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId);
|
|
|
|
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;
|
|
}
|
|
|
|
private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId)
|
|
{
|
|
if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
|
{
|
|
if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
|
{
|
|
// If the user has two-step login enabled, we skip checking the 2FA policy
|
|
return;
|
|
}
|
|
|
|
var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(user.Id);
|
|
if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId))
|
|
{
|
|
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
|
{
|
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
|
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId))
|
|
{
|
|
throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account.");
|
|
}
|
|
}
|
|
}
|
|
}
|