1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-27 14:24:56 -05:00

Merge remote-tracking branch 'origin/main' into ac/pm-13274/unified-adding-a-group-to-a-collection-returns-500-error---but-works

This commit is contained in:
Thomas Rittson 2025-05-13 13:51:58 +10:00
commit eac9f13100
No known key found for this signature in database
GPG Key ID: CDDDA03861C35E27
138 changed files with 1201 additions and 2239 deletions

View File

@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -325,6 +329,14 @@ Global
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -377,6 +389,8 @@ Global
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}

View File

@ -4,6 +4,7 @@
"rollForward": "latestFeature" "rollForward": "latestFeature"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "4.1.0" "Microsoft.Build.Traversal": "4.1.0",
"Microsoft.Build.Sql": "0.1.9-preview"
} }
} }

View File

@ -4,7 +4,6 @@ using Bit.Admin.Enums;
using Bit.Admin.Models; using Bit.Admin.Models;
using Bit.Admin.Services; using Bit.Admin.Services;
using Bit.Admin.Utilities; using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -89,7 +88,7 @@ public class UsersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain)); return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
} }
@ -106,7 +105,7 @@ public class UsersController : Controller
var billingInfo = await _paymentService.GetBillingAsync(user); var billingInfo = await _paymentService.GetBillingAsync(user);
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id);
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
@ -167,7 +166,6 @@ public class UsersController : Controller
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
[RequirePermission(Permission.User_NewDeviceException_Edit)] [RequirePermission(Permission.User_NewDeviceException_Edit)]
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id) public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
{ {
var user = await _userRepository.GetByIdAsync(id); var user = await _userRepository.GetByIdAsync(id);
@ -179,12 +177,4 @@ public class UsersController : Controller
await _userService.ToggleNewDeviceVerificationException(user.Id); await _userService.ToggleNewDeviceVerificationException(user.Id);
return RedirectToAction("Edit", new { id }); return RedirectToAction("Edit", new { id });
} }
// TODO: Feature flag to be removed in PM-14207
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
{
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
? await _userService.IsClaimedByAnyOrganizationAsync(userId)
: null;
}
} }

View File

@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("{id}/delete-account")] [HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")] [HttpPost("{id}/delete-account")]
public async Task DeleteAccount(Guid orgId, Guid id) public async Task DeleteAccount(Guid orgId, Guid id)
@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
} }
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
[HttpDelete("delete-account")] [HttpDelete("delete-account")]
[HttpPost("delete-account")] [HttpPost("delete-account")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
@ -760,11 +758,6 @@ public class OrganizationUsersController : Controller
private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds) private async Task<IDictionary<Guid, bool>> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return userIds.ToDictionary(kvp => kvp, kvp => false);
}
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds); var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
return usersOrganizationClaimedStatus; return usersOrganizationClaimedStatus;
} }

View File

@ -279,8 +279,7 @@ public class OrganizationsController : Controller
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
{ {
throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details."); throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details.");
} }

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@ -79,7 +78,7 @@ public class PoliciesController : Controller
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) if (policy.Type is PolicyType.SingleOrg)
{ {
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery); return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
} }

View File

@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType; ProviderType = organization.ProviderType;
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && IsAdminInitiated = organization.IsAdminInitiated ?? false;
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization); .UsersCanSponsor(organization);
ProductTierType = organization.PlanType.GetProductTier(); ProductTierType = organization.PlanType.GetProductTier();
@ -135,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary> /// <summary>
/// Obsolete. /// Obsolete.
///
/// See <see cref="UserIsClaimedByOrganization"/> /// See <see cref="UserIsClaimedByOrganization"/>
/// </summary> /// </summary>
[Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")] [Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")]
@ -145,16 +145,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
set => UserIsClaimedByOrganization = value; set => UserIsClaimedByOrganization = value;
} }
/// <summary> /// <summary>
/// Indicates if the organization claims the user. /// Indicates if the user is claimed by the organization.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it. /// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member.
/// The organization must be enabled and able to have verified domains. /// The organization must be enabled and able to have verified domains.
/// </remarks> /// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsClaimedByOrganization { get; set; } public bool UserIsClaimedByOrganization { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public bool IsAdminInitiated { get; set; }
} }

View File

@ -76,7 +76,7 @@ public class MembersController : Controller
{ {
return new NotFoundResult(); return new NotFoundResult();
} }
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
collections); collections);
return new JsonResult(response); return new JsonResult(response);
} }
@ -185,7 +185,7 @@ public class MembersController : Controller
{ {
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
response = new MemberResponseModel(existingUserDetails, response = new MemberResponseModel(existingUserDetails,
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations); await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
} }
else else
{ {

View File

@ -4,8 +4,6 @@
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS> <ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -45,6 +46,7 @@ public class AccountsController : Controller
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator; private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
@ -68,6 +70,7 @@ public class AccountsController : Controller
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
IRotateUserKeyCommand rotateUserKeyCommand, IRotateUserKeyCommand rotateUserKeyCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService, IFeatureService featureService,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator, IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator, IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
@ -87,6 +90,7 @@ public class AccountsController : Controller
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_rotateUserKeyCommand = rotateUserKeyCommand; _rotateUserKeyCommand = rotateUserKeyCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService; _featureService = featureService;
_cipherValidator = cipherValidator; _cipherValidator = cipherValidator;
_folderValidator = folderValidator; _folderValidator = folderValidator;
@ -389,7 +393,7 @@ public class AccountsController : Controller
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
ProviderUserStatusType.Confirmed); ProviderUserStatusType.Confirmed);
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -423,7 +427,7 @@ public class AccountsController : Controller
await _userService.SaveUserAsync(model.ToUser(user)); await _userService.SaveUserAsync(model.ToUser(user));
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -442,7 +446,7 @@ public class AccountsController : Controller
} }
await _userService.SaveUserAsync(model.ToUser(user), true); await _userService.SaveUserAsync(model.ToUser(user), true);
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
@ -514,9 +518,8 @@ public class AccountsController : Controller
} }
else else
{ {
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. // Check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{ {
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
} }
@ -693,7 +696,6 @@ public class AccountsController : Controller
} }
} }
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[AllowAnonymous] [AllowAnonymous]
[HttpPost("resend-new-device-otp")] [HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)

View File

@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
[Route("accounts")] [Route("accounts")]
[Authorize("Application")] [Authorize("Application")]
public class AccountsController( public class AccountsController(
IUserService userService) : Controller IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
{ {
[HttpPost("premium")] [HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync( public async Task<PaymentResponseModel> PostPremiumAsync(
@ -56,7 +58,7 @@ public class AccountsController(
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user); var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);

View File

@ -1,4 +1,5 @@
#nullable enable #nullable enable
using System.Diagnostics;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses; using Bit.Api.Billing.Models.Responses;
@ -292,6 +293,7 @@ public class OrganizationBillingController(
sale.SubscriptionSetup.SkipTrial = true; sale.SubscriptionSetup.SkipTrial = true;
await organizationBillingService.Finalize(sale); await organizationBillingService.Finalize(sale);
var org = await organizationRepository.GetByIdAsync(organizationId); var org = await organizationRepository.GetByIdAsync(organizationId);
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
if (organizationSignup.PaymentMethodType != null) if (organizationSignup.PaymentMethodType != null)
{ {
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);

View File

@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller
} }
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s => return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))); sponsorships
.Where(s => s.IsAdminInitiated)
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
);
} }

View File

@ -1086,9 +1086,8 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. // Check if the user is claimed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
{ {
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
} }

View File

@ -3,6 +3,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -37,6 +38,7 @@ public class SyncController : Controller
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
public SyncController( public SyncController(
IUserService userService, IUserService userService,
@ -51,7 +53,8 @@ public class SyncController : Controller
GlobalSettings globalSettings, GlobalSettings globalSettings,
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService, IFeatureService featureService,
IApplicationCacheService applicationCacheService) IApplicationCacheService applicationCacheService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
{ {
_userService = userService; _userService = userService;
_folderRepository = folderRepository; _folderRepository = folderRepository;
@ -66,6 +69,7 @@ public class SyncController : Controller
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService; _featureService = featureService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
} }
[HttpGet("")] [HttpGet("")]
@ -102,7 +106,7 @@ public class SyncController : Controller
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
} }
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);

View File

@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; } public bool UseRiskInsights { get; set; }
public bool UseAdminSponsoredFamilies { get; set; } public bool UseAdminSponsoredFamilies { get; set; }
public bool? IsAdminInitiated { get; set; }
} }

View File

@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
IDnsResolverService dnsResolverService, IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IFeatureService featureService,
ICurrentContext currentContext, ICurrentContext currentContext,
ISavePolicyCommand savePolicyCommand, ISavePolicyCommand savePolicyCommand,
IMailService mailService, IMailService mailService,
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser) private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser)
{ {
if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
{ await SendVerifiedDomainUserEmailAsync(domain);
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser);
await SendVerifiedDomainUserEmailAsync(domain);
}
} }
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) => private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) =>

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Enums; using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory; private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
public AcceptOrgUserCommand( public AcceptOrgUserCommand(
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
IPolicyService policyService, IPolicyService policyService,
IMailService mailService, IMailService mailService,
IUserRepository userRepository, IUserRepository userRepository,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory) IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
{ {
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
_policyService = policyService; _policyService = policyService;
_mailService = mailService; _mailService = mailService;
_userRepository = userRepository; _userRepository = userRepository;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
} }
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
} }
// Enforce Two Factor Authentication Policy of organization user is trying to join // Enforce Two Factor Authentication Policy of organization user is trying to join
if (!await userService.TwoFactorIsEnabledAsync(user)) if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{ {
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);

View File

@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
throw new BadRequestException(RemoveAdminByCustomUserErrorMessage); throw new BadRequestException(RemoveAdminByCustomUserErrorMessage);
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null) if (deletingUserId.HasValue && eventSystemUser == null)
{ {
var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id }); var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id });
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed) if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
} }
var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null var claimedStatus = deletingUserId.HasValue && eventSystemUser == null
? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id)) ? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id))
: filteredUsers.ToDictionary(u => u.Id, u => false); : filteredUsers.ToDictionary(u => u.Id, u => false);
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>(); var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();

View File

@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
{ {
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) var currentUser = _currentContext.UserId ?? Guid.Empty;
{ var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
var currentUser = _currentContext.UserId ?? Guid.Empty; await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
} }
} }
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
_mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email)));
} }
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
// Remove non-compliant users
var savingUserId = _currentContext.UserId;
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException(OrganizationNotFoundErrorMessage);
}
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited &&
ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner &&
ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId
).ToList();
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId!.Value));
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
}
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{ {
if (policyUpdate is not { Enabled: true }) if (policyUpdate is not { Enabled: true })
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
return validateDecryptionErrorMessage; return validateDecryptionErrorMessage;
} }
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
{ {
return ClaimedDomainSingleOrganizationRequiredErrorMessage; return ClaimedDomainSingleOrganizationRequiredErrorMessage;
} }

View File

@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IFeatureService _featureService;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."; public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.";
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
ICurrentContext currentContext, ICurrentContext currentContext,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IFeatureService featureService,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
{ {
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_currentContext = currentContext; _currentContext = currentContext;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_featureService = featureService;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
} }
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
{ {
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) var currentUser = _currentContext.UserId ?? Guid.Empty;
{ var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
var currentUser = _currentContext.UserId ?? Guid.Empty; await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
}
else
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
} }
} }
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
_mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email)));
} }
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var savingUserId = _currentContext.UserId;
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId);
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
.twoFactorIsEnabled;
if (!userTwoFactorEnabled)
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org!.DisplayName(), orgUser.Email);
}
}
}
private static bool MembersWithNoMasterPasswordWillLoseAccess( private static bool MembersWithNoMasterPasswordWillLoseAccess(
IEnumerable<OrganizationUserUserDetails> orgUserDetails, IEnumerable<OrganizationUserUserDetails> orgUserDetails,
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>

View File

@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService
//Send email to administrators //Send email to administrators
if (adminEmails.Count > 0) if (adminEmails.Count > 0)
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
{ domain.OrganizationId.ToString(), domain.DomainName);
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
else
{
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
domain.OrganizationId.ToString(), domain.DomainName);
}
} }
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName); _logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);

View File

@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte
Email = 1, Email = 1,
Duo = 2, Duo = 2,
YubiKey = 3, YubiKey = 3,
U2f = 4, // Deprecated [Obsolete("Deprecated in favor of WebAuthn.")]
U2f = 4,
Remember = 5, Remember = 5,
OrganizationDuo = 6, OrganizationDuo = 6,
WebAuthn = 7, WebAuthn = 7,

View File

@ -1,6 +1,5 @@
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
{ {
private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}"; private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}";
private readonly IServiceProvider _serviceProvider;
private readonly IDistributedCache _distributedCache; private readonly IDistributedCache _distributedCache;
private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;
public AuthenticatorTokenProvider( public AuthenticatorTokenProvider(
IServiceProvider serviceProvider,
[FromKeyedServices("persistent")] [FromKeyedServices("persistent")]
IDistributedCache distributedCache) IDistributedCache distributedCache)
{ {
_serviceProvider = serviceProvider;
_distributedCache = distributedCache; _distributedCache = distributedCache;
_distributedCacheEntryOptions = new DistributedCacheEntryOptions _distributedCacheEntryOptions = new DistributedCacheEntryOptions
{ {
@ -29,15 +25,14 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
}; };
} }
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"])) if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"]))
{ {
return false; return Task.FromResult(false);
} }
return await _serviceProvider.GetRequiredService<IUserService>() return Task.FromResult(authenticatorProvider.Enabled);
.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user);
} }
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)

View File

@ -17,7 +17,7 @@ public class DuoUniversalTokenProvider(
{ {
/// <summary> /// <summary>
/// We need the IServiceProvider to resolve the <see cref="IUserService"/>. There is a complex dependency dance /// We need the IServiceProvider to resolve the <see cref="IUserService"/>. There is a complex dependency dance
/// occurring between <see cref="IUserService"/>, which extends the <see cref="UserManager{User}"/>, and the usage /// occurring between <see cref="IUserService"/>, which extends the <see cref="UserManager{User}"/>, and the usage
/// of the <see cref="UserManager{User}"/> within this class. Trying to resolve the <see cref="IUserService"/> using /// of the <see cref="UserManager{User}"/> within this class. Trying to resolve the <see cref="IUserService"/> using
/// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the
/// problem. /// problem.
@ -29,12 +29,13 @@ public class DuoUniversalTokenProvider(
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
var userService = _serviceProvider.GetRequiredService<IUserService>(); var userService = _serviceProvider.GetRequiredService<IUserService>();
var provider = await GetDuoTwoFactorProvider(user, userService); var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService);
if (provider == null) if (duoUniversalTokenProvider == null)
{ {
return false; return false;
} }
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
return duoUniversalTokenProvider.Enabled;
} }
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
@ -58,7 +59,7 @@ public class DuoUniversalTokenProvider(
} }
/// <summary> /// <summary>
/// Get the Duo Two Factor Provider for the user if they have access to Duo /// Get the Duo Two Factor Provider for the user if they have premium access to Duo
/// </summary> /// </summary>
/// <param name="user">Active User</param> /// <param name="user">Active User</param>
/// <returns>null or Duo TwoFactorProvider</returns> /// <returns>null or Duo TwoFactorProvider</returns>

View File

@ -1,7 +1,6 @@
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders;
public class EmailTwoFactorTokenProvider : EmailTokenProvider public class EmailTwoFactorTokenProvider : EmailTokenProvider
{ {
private readonly IServiceProvider _serviceProvider;
public EmailTwoFactorTokenProvider( public EmailTwoFactorTokenProvider(
IServiceProvider serviceProvider,
[FromKeyedServices("persistent")] [FromKeyedServices("persistent")]
IDistributedCache distributedCache) : IDistributedCache distributedCache) :
base(distributedCache) base(distributedCache)
{ {
_serviceProvider = serviceProvider;
TokenAlpha = false; TokenAlpha = false;
TokenNumeric = true; TokenNumeric = true;
TokenLength = 6; TokenLength = 6;
} }
public override async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (!HasProperMetaData(provider)) if (!HasProperMetaData(emailTokenProvider))
{ {
return false; return Task.FromResult(false);
} }
return await _serviceProvider.GetRequiredService<IUserService>(). return Task.FromResult(emailTokenProvider.Enabled);
TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user);
} }
public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)

View File

@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
var userService = _serviceProvider.GetRequiredService<IUserService>();
var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
// null check happens in this method
if (!HasProperMetaData(webAuthnProvider)) if (!HasProperMetaData(webAuthnProvider))
{ {
return false; return Task.FromResult(false);
} }
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user); return Task.FromResult(webAuthnProvider.Enabled);
} }
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
var keys = LoadKeys(provider); var keys = LoadKeys(provider);
if (!provider.MetaData.ContainsKey("login")) if (!provider.MetaData.TryGetValue("login", out var value))
{ {
return false; return false;
} }
@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token, var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var jsonOptions = provider.MetaData["login"].ToString(); var jsonOptions = value.ToString();
var options = AssertionOptions.FromJson(jsonOptions); var options = AssertionOptions.FromJson(jsonOptions);
var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));
@ -126,6 +125,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
} }
/// <summary>
/// Checks if the provider has proper metadata.
/// This is used to determine if the provider has been properly configured.
/// </summary>
/// <param name="provider"></param>
/// <returns>true if metadata is present; false if empty or null</returns>
private bool HasProperMetaData(TwoFactorProvider provider) private bool HasProperMetaData(TwoFactorProvider provider)
{ {
return provider?.MetaData?.Any() ?? false; return provider?.MetaData?.Any() ?? false;

View File

@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
{ {
// Ensure the user has access to premium
var userService = _serviceProvider.GetRequiredService<IUserService>(); var userService = _serviceProvider.GetRequiredService<IUserService>();
if (!await userService.CanAccessPremium(user)) if (!await userService.CanAccessPremium(user))
{ {
return false; return false;
} }
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); // Check if the user has a YubiKey provider configured
if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);
if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)
{ {
return false; return false;
} }
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user); return yubicoProvider.Enabled;
} }
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user) public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)

View File

@ -1,7 +1,7 @@
using Bit.Core.Context; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -167,7 +167,7 @@ public class UserStore :
public async Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) public async Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken)
{ {
return await _serviceProvider.GetRequiredService<IUserService>().TwoFactorIsEnabledAsync(user); return await _serviceProvider.GetRequiredService<ITwoFactorIsEnabledQuery>().TwoFactorIsEnabledAsync(user);
} }
public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken)

View File

@ -1,6 +0,0 @@
namespace Bit.Core.Auth.Models.Api;
public interface ICaptchaProtectedModel
{
string CaptchaResponse { get; set; }
}

View File

@ -1,9 +0,0 @@
namespace Bit.Core.Auth.Models.Business;
public class CaptchaResponse
{
public bool Success { get; set; }
public bool MaybeBot { get; set; }
public bool IsBot { get; set; }
public double Score { get; set; }
}

View File

@ -1,43 +0,0 @@
using System.Text.Json.Serialization;
using Bit.Core.Entities;
using Bit.Core.Tokens;
namespace Bit.Core.Auth.Models.Business.Tokenables;
public class HCaptchaTokenable : ExpiringTokenable
{
private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes
public const string ClearTextPrefix = "BWCaptchaBypass_";
public const string DataProtectorPurpose = "CaptchaServiceDataProtector";
public const string TokenIdentifier = "CaptchaBypassToken";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }
public string Email { get; set; }
[JsonConstructor]
public HCaptchaTokenable()
{
ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours);
}
public HCaptchaTokenable(User user) : this()
{
Id = user?.Id ?? default;
Email = user?.Email;
}
public bool TokenIsValid(User user)
{
if (Id == default || Email == default || user == null)
{
return false;
}
return Id == user.Id &&
Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase);
}
// Validates deserialized
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email);
}

View File

@ -4,9 +4,10 @@ using Bit.Core.Tokens;
namespace Bit.Core.Auth.Models.Business.Tokenables; namespace Bit.Core.Auth.Models.Business.Tokenables;
// This token just provides a verifiable authN mechanism for the API service /// <summary>
// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be /// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync
// used maliciously. /// anonymous endpoint so it cannot used maliciously.
/// </summary>
public class SsoEmail2faSessionTokenable : ExpiringTokenable public class SsoEmail2faSessionTokenable : ExpiringTokenable
{ {
// Just over 2 min expiration (client expires session after 2 min) // Just over 2 min expiration (client expires session after 2 min)

View File

@ -1,10 +1,18 @@
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Services;
namespace Bit.Core.Auth.Models; namespace Bit.Core.Auth.Models;
public interface ITwoFactorProvidersUser public interface ITwoFactorProvidersUser
{ {
string TwoFactorProviders { get; } string TwoFactorProviders { get; }
/// <summary>
/// Get the two factor providers for the user. Currently it can be assumed providers are enabled
/// if they exists in the dictionary. When two factor providers are disabled they are removed
/// from the dictionary. <see cref="IUserService.DisableTwoFactorProviderAsync"/>
/// <see cref="IOrganizationService.DisableTwoFactorProviderAsync"/>
/// </summary>
/// <returns>Dictionary of providers with the type enum as the key</returns>
Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders(); Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders();
Guid? GetUserId(); Guid? GetUserId();
bool GetPremium(); bool GetPremium();

View File

@ -1,15 +0,0 @@
using Bit.Core.Auth.Models.Business;
using Bit.Core.Context;
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
public interface ICaptchaValidationService
{
string SiteKey { get; }
string SiteKeyResponseKeyName { get; }
bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null);
Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress,
User user = null);
string GenerateCaptchaBypassToken(User user);
}

View File

@ -1,132 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Bit.Core.Auth.Models.Business;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Auth.Services;
public class HCaptchaValidationService : ICaptchaValidationService
{
private readonly ILogger<HCaptchaValidationService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly GlobalSettings _globalSettings;
private readonly IDataProtectorTokenFactory<HCaptchaTokenable> _tokenizer;
public HCaptchaValidationService(
ILogger<HCaptchaValidationService> logger,
IHttpClientFactory httpClientFactory,
IDataProtectorTokenFactory<HCaptchaTokenable> tokenizer,
GlobalSettings globalSettings)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_globalSettings = globalSettings;
_tokenizer = tokenizer;
}
public string SiteKeyResponseKeyName => "HCaptcha_SiteKey";
public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey;
public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user));
public async Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
User user = null)
{
var response = new CaptchaResponse { Success = false };
if (string.IsNullOrWhiteSpace(captchaResponse))
{
return response;
}
if (user != null && ValidateCaptchaBypassToken(captchaResponse, user))
{
response.Success = true;
return response;
}
var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService");
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri("https://hcaptcha.com/siteverify"),
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) },
{ "secret", _globalSettings.Captcha.HCaptchaSecretKey },
{ "sitekey", SiteKey },
{ "remoteip", clientIpAddress }
})
};
HttpResponseMessage responseMessage;
try
{
responseMessage = await httpClient.SendAsync(requestMessage);
}
catch (Exception e)
{
_logger.LogError(11389, e, "Unable to verify with HCaptcha.");
return response;
}
if (!responseMessage.IsSuccessStatusCode)
{
return response;
}
using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync<HCaptchaResponse>();
response.Success = hcaptchaResponse.Success;
var score = hcaptchaResponse.Score.GetValueOrDefault();
response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold;
response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold;
response.Score = score;
return response;
}
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null)
{
if (user == null)
{
return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired;
}
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
var requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified &&
user.CreationDate < DateTime.UtcNow.AddHours(-24);
return currentContext.IsBot ||
_globalSettings.Captcha.ForceCaptchaRequired ||
requireOnCloud ||
failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling;
}
private static bool TokenIsValidApiKey(string bypassToken, User user) =>
!string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken;
private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user)
{
return _tokenizer.TryUnprotect(encryptedToken, out var data) &&
data.Valid && data.TokenIsValid(user);
}
private bool ValidateCaptchaBypassToken(string bypassToken, User user) =>
TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user);
public class HCaptchaResponse : IDisposable
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("score")]
public double? Score { get; set; }
[JsonPropertyName("score_reason")]
public List<string> ScoreReason { get; set; }
public void Dispose() { }
}
}

View File

@ -1,18 +0,0 @@
using Bit.Core.Auth.Models.Business;
using Bit.Core.Context;
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
public class NoopCaptchaValidationService : ICaptchaValidationService
{
public string SiteKeyResponseKeyName => null;
public string SiteKey => null;
public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false;
public string GenerateCaptchaBypassToken(User user) => "";
public Task<CaptchaResponse> ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress,
User user = null)
{
return Task.FromResult(new CaptchaResponse { Success = true });
}
}

View File

@ -2,6 +2,7 @@
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
public interface ITwoFactorIsEnabledQuery public interface ITwoFactorIsEnabledQuery
{ {
/// <summary> /// <summary>
@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery
/// <typeparam name="T">The type of user in the list. Must implement <see cref="ITwoFactorProvidersUser"/>.</typeparam> /// <typeparam name="T">The type of user in the list. Must implement <see cref="ITwoFactorProvidersUser"/>.</typeparam>
Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser; Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser;
/// <summary> /// <summary>
/// Returns whether two factor is enabled for the user. /// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium.
/// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled.
/// </summary> /// </summary>
/// <param name="user">The user to check.</param> /// <param name="user">The user to check.</param>
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);

View File

@ -1,17 +1,13 @@
using Bit.Core.Auth.Models; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery
{ {
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository = userRepository;
public TwoFactorIsEnabledQuery(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds) public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
{ {
@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
return result; return result;
} }
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList()); var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]);
foreach (var userDetail in userDetails) foreach (var userDetail in userDetails)
{ {
var hasTwoFactor = false; result.Add(
var providers = userDetail.GetTwoFactorProviders(); (userDetail.Id,
if (providers != null) await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(),
{ () => Task.FromResult(userDetail.HasPremiumAccess))
// Get all enabled providers )
var enabledProviderKeys = from provider in providers );
where provider.Value?.Enabled ?? false
select provider.Key;
// Find the first provider that is enabled and passes the premium check
hasTwoFactor = enabledProviderKeys
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
.FirstOrDefault();
}
result.Add((userDetail.Id, hasTwoFactor));
} }
return result; return result;
@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
return false; return false;
} }
var providers = user.GetTwoFactorProviders(); return await TwoFactorEnabledAsync(
if (providers == null || !providers.Any()) user.GetTwoFactorProviders(),
async () =>
{
var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value);
return calcUser?.HasPremiumAccess ?? false;
});
}
/// <summary>
/// Checks to see what kind of two-factor is enabled.
/// We use a delegate to check if the user has premium access, since there are multiple ways to
/// determine if a user has premium access.
/// </summary>
/// <param name="providers">dictionary of two factor providers</param>
/// <param name="hasPremiumAccessDelegate">function to check if the user has premium access</param>
/// <returns> true if the user has two factor enabled; false otherwise;</returns>
private async static Task<bool> TwoFactorEnabledAsync(
Dictionary<TwoFactorProviderType, TwoFactorProvider> providers,
Func<Task<bool>> hasPremiumAccessDelegate)
{
// If there are no providers, then two factor is not enabled
if (providers == null || providers.Count == 0)
{ {
return false; return false;
} }
// Get all enabled providers // Get all enabled providers
var enabledProviderKeys = providers // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.
.Where(provider => provider.Value?.Enabled ?? false) var enabledProviderKeys = from provider in providers
.Select(provider => provider.Key); where provider.Value?.Enabled ?? false
select provider.Key;
// If no providers are enabled then two factor is not enabled
if (!enabledProviderKeys.Any()) if (!enabledProviderKeys.Any())
{ {
return false; return false;
} }
// Determine if any enabled provider passes the premium check // If there are only premium two factor options then standard two factor is not enabled
var hasTwoFactor = enabledProviderKeys var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium);
.Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type)) if (onlyHasPremiumTwoFactor)
.FirstOrDefault();
// If no enabled provider passes the check, check the repository for organization premium access
if (!hasTwoFactor)
{ {
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List<Guid> { userId.Value }); // There are no Standard two factor options, check if the user has premium access
var userDetail = userDetails.FirstOrDefault(); // If the user has premium access, then two factor is enabled
var premiumAccess = await hasPremiumAccessDelegate();
if (userDetail != null) return premiumAccess;
{
hasTwoFactor = enabledProviderKeys
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
.FirstOrDefault();
}
} }
return hasTwoFactor; // The user has at least one non-premium two factor option
return true;
} }
} }

View File

@ -1,36 +0,0 @@
using Bit.Core.Auth.Models.Api;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Auth.Utilities;
public class CaptchaProtectedAttribute : ActionFilterAttribute
{
public string ModelParameterName { get; set; } = "model";
public override void OnActionExecuting(ActionExecutingContext context)
{
var currentContext = context.HttpContext.RequestServices.GetRequiredService<ICurrentContext>();
var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService<ICaptchaValidationService>();
if (captchaValidationService.RequireCaptchaValidation(currentContext, null))
{
var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse;
if (string.IsNullOrWhiteSpace(captchaResponse))
{
throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey);
}
var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse,
currentContext.IpAddress, null).GetAwaiter().GetResult();
if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot)
{
throw new BadRequestException("Captcha is invalid. Please refresh and try again");
}
}
}
}

View File

@ -59,7 +59,7 @@ public interface IProviderBillingService
int seatAdjustment); int seatAdjustment);
/// <summary> /// <summary>
/// Determines whether the provided <paramref name="seatAdjustment"/> will result in a purchase for the <paramref name="provider"/>'s <see cref="planType"/>. /// Determines whether the provided <paramref name="seatAdjustment"/> will result in a purchase for the <paramref name="provider"/>'s <see cref="PlanType"/>.
/// Seat adjustments that result in purchases include: /// Seat adjustments that result in purchases include:
/// <list type="bullet"> /// <list type="bullet">
/// <item>The <paramref name="provider"/> going from below the seat minimum to above the seat minimum for the provided <paramref name="planType"/></item> /// <item>The <paramref name="provider"/> going from below the seat minimum to above the seat minimum for the provided <paramref name="planType"/></item>

View File

@ -5,14 +5,12 @@ using Bit.Core.Entities;
using Bit.Core.Models.BitStripe; using Bit.Core.Models.BitStripe;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Billing.Services.Implementations; namespace Bit.Core.Billing.Services.Implementations;
public class PaymentHistoryService( public class PaymentHistoryService(
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository) : IPaymentHistoryService
ILogger<PaymentHistoryService> logger) : IPaymentHistoryService
{ {
public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync( public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
ISubscriber subscriber, ISubscriber subscriber,

View File

@ -115,7 +115,6 @@ public static class FeatureFlagKeys
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
public const string EmailVerification = "email-verification"; public const string EmailVerification = "email-verification";
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
public const string NewDeviceVerification = "new-device-verification";
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";

View File

@ -3,8 +3,6 @@
<PropertyGroup> <PropertyGroup>
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute> <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1574;CS9113;CS1998</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@ -77,4 +75,8 @@
<Folder Include="Resources\" /> <Folder Include="Resources\" />
<Folder Include="Properties\" /> <Folder Include="Properties\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Infrastructure.IntegrationTest" />
</ItemGroup>
</Project> </Project>

View File

@ -128,6 +128,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow; public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow;
/// <summary>
/// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary.
/// </summary>
/// <returns>Dictionary of TwoFactor providers</returns>
public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders() public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()
{ {
if (string.IsNullOrWhiteSpace(TwoFactorProviders)) if (string.IsNullOrWhiteSpace(TwoFactorProviders))
@ -137,19 +141,17 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
try try
{ {
if (_twoFactorProviders == null) _twoFactorProviders ??=
{ JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
_twoFactorProviders = TwoFactorProviders);
JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
TwoFactorProviders);
}
// U2F is no longer supported, and all users keys should have been migrated to WebAuthn. /*
// To prevent issues with accounts being prompted for unsupported U2F we remove them U2F is no longer supported, and all users keys should have been migrated to WebAuthn.
if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) To prevent issues with accounts being prompted for unsupported U2F we remove them.
{ This will probably exist in perpetuity since there is no way to know for sure if any
_twoFactorProviders.Remove(TwoFactorProviderType.U2f); given user does or doesn't have this enabled. It is a non-zero chance.
} */
_twoFactorProviders?.Remove(TwoFactorProviderType.U2f);
return _twoFactorProviders; return _twoFactorProviders;
} }
@ -169,6 +171,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
return Premium; return Premium;
} }
/// <summary>
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
/// </summary>
/// <param name="providers">Dictionary of Two Factor providers</param>
public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers) public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)
{ {
// When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider. // When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider.
@ -176,20 +182,21 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
_twoFactorProviders = providers; _twoFactorProviders = providers;
} }
public void ClearTwoFactorProviders() /// <summary>
{ /// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor
SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>()); /// configured it will still be found, even if the user's premium subscription has ended.
} /// </summary>
/// <param name="provider">TwoFactor provider being searched for</param>
/// <returns>TwoFactorProvider if found; null otherwise.</returns>
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
{ {
var providers = GetTwoFactorProviders(); var providers = GetTwoFactorProviders();
if (providers == null || !providers.ContainsKey(provider)) if (providers == null || !providers.TryGetValue(provider, out var value))
{ {
return null; return null;
} }
return providers[provider]; return value;
} }
public long StorageBytesRemaining() public long StorageBytesRemaining()

View File

@ -1,31 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b> {{AffectedEmail}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,13 +0,0 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in.
If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection.
{{/BasicTextLayout}}

View File

@ -1,31 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Additional security has been placed on your Bitwarden account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Account:</b> {{AffectedEmail}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Date:</b> {{TheDate}} at {{TheTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">IP Address:</b> {{IpAddress}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a <a target="_blank" clicktracking=off href="https://bitwarden.com/help/two-step-recovery-code/" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">recovery code</a>.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
If this was not you, you should <a target="_blank" clicktracking=off href="https://bitwarden.com/help/master-password/#change-master-password" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">change your master password</a> immediately. You can view our tips for selecting a secure master password <a target="_blank" clicktracking=off href="https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #175DDC; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; text-decoration: underline;">here</a>.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,13 +0,0 @@
{{#>BasicTextLayout}}
Additional security has been placed on your Bitwarden account.
We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha.
Account: {{AffectedEmail}}
Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
IP Address: {{IpAddress}}
If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/).
If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/).
{{/BasicTextLayout}}

View File

@ -1,27 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top">
The domain {{DomainName}} in your Bitwarden organization could not be verified.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
The domain will be removed from your organization in 7 days if it is not verified.
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
Manage Domains
</a>
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,10 +0,0 @@
{{#>BasicTextLayout}}
The domain {{DomainName}} in your Bitwarden organization could not be verified.
Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization.
The domain will be removed from your organization in 7 days if it is not verified.
{{Url}}
{{/BasicTextLayout}}

View File

@ -1,9 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you are a part of another organization. The {{OrganizationName}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,5 +0,0 @@
{{#>BasicTextLayout}}
Your user account has been removed from the {{OrganizationName}} organization because you are a part of another
organization. The {{OrganizationName}} has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations, or join with a
new account.
{{/BasicTextLayout}}

View File

@ -1,15 +0,0 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top" align="left">
Your user account has been removed from the <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{OrganizationName}}</b> organization because you do not have two-step login configured. Before you can re-join this organization you need to set up two-step login on your user account.
</td>
</tr>
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="left">
Learn how to enable two-step login on your user account at
<a target="_blank" href="https://help.bitwarden.com/article/setup-two-step-login/" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">https://help.bitwarden.com/article/setup-two-step-login/</a>
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -1,7 +0,0 @@
{{#>BasicTextLayout}}
Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login
configured. Before you can re-join this organization you need to set up two-step login on your user account.
Learn how to enable two-step login on your user account at
<https://help.bitwarden.com/article/setup-two-step-login/>
{{/BasicTextLayout}}

View File

@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel
public bool ToDelete { get; set; } public bool ToDelete { get; set; }
public bool CloudSponsorshipRemoved { get; set; } public bool CloudSponsorshipRemoved { get; set; }
public bool IsAdminInitiated { get; set; }
public OrganizationSponsorshipResponseModel() { } public OrganizationSponsorshipResponseModel() { }
@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel
ValidUntil = sponsorshipData.ValidUntil; ValidUntil = sponsorshipData.ValidUntil;
ToDelete = sponsorshipData.ToDelete; ToDelete = sponsorshipData.ToDelete;
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
IsAdminInitiated = sponsorshipData.IsAdminInitiated;
} }
public OrganizationSponsorshipData ToOrganizationSponsorship() public OrganizationSponsorshipData ToOrganizationSponsorship()
@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel
LastSyncDate = LastSyncDate, LastSyncDate = LastSyncDate,
ValidUntil = ValidUntil, ValidUntil = ValidUntil,
ToDelete = ToDelete, ToDelete = ToDelete,
CloudSponsorshipRemoved = CloudSponsorshipRemoved CloudSponsorshipRemoved = CloudSponsorshipRemoved,
IsAdminInitiated = IsAdminInitiated,
}; };
} }

View File

@ -25,6 +25,16 @@ public interface IUserRepository : IRepository<User, Guid>
/// </summary> /// </summary>
Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids); Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);
/// <summary> /// <summary>
/// Retrieves the data for the requested user ID and includes additional property indicating
/// whether the user has premium access directly or through an organization.
///
/// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query
/// for a single user.
/// </summary>
/// <param name="userId">The user ID to retrieve data for.</param>
/// <returns>User data with calculated premium access; null if nothing is found</returns>
Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid userId);
/// <summary>
/// Sets a new user key and updates all encrypted data. /// Sets a new user key and updates all encrypted data.
/// <para>Warning: Any user key encrypted data not included will be lost.</para> /// <para>Warning: Any user key encrypted data not included will be lost.</para>
/// </summary> /// </summary>

View File

@ -40,7 +40,6 @@ public interface IMailService
Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, bool hasAccessSecretsManager = false);
Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false);
Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email);
Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email);
Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email);
Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email);
@ -61,7 +60,6 @@ public interface IMailService
Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null); Task SendLicenseExpiredAsync(IEnumerable<string> emails, string? organizationName = null);
Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip); Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip);
Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip); Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip);
Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email);
Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token); Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token);
Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email); Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email);
Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email); Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email);
@ -88,9 +86,6 @@ public interface IMailService
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
Task SendOTPEmailAsync(string email, string token); Task SendOTPEmailAsync(string email, string token);
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName); Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails); Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);

View File

@ -71,11 +71,13 @@ public interface IUserService
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
int? version = null); int? version = null);
Task<bool> CheckPasswordAsync(User user, string password); Task<bool> CheckPasswordAsync(User user, string password);
/// <summary>
/// Checks if the user has access to premium features, either through a personal subscription or through an organization.
/// </summary>
/// <param name="user">user being acted on</param>
/// <returns>true if they can access premium; false otherwise.</returns>
Task<bool> CanAccessPremium(ITwoFactorProvidersUser user); Task<bool> CanAccessPremium(ITwoFactorProvidersUser user);
Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user); Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user);
[Obsolete("Use ITwoFactorIsEnabledQuery instead.")]
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user);
Task<string> GenerateSignInTokenAsync(User user, string purpose); Task<string> GenerateSignInTokenAsync(User user, string purpose);
Task<IdentityResult> UpdatePasswordHash(User user, string newPassword, Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
@ -131,16 +133,11 @@ public interface IUserService
/// verified domains of that organization, and the user is a member of it. /// verified domains of that organization, and the user is a member of it.
/// The organization must be enabled and able to have verified domains. /// The organization must be enabled and able to have verified domains.
/// </remarks> /// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId); Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId);
/// <summary> /// <summary>
/// Verify whether the new email domain meets the requirements for managed users. /// Verify whether the new email domain meets the requirements for managed users.
/// </summary> /// </summary>
/// <remarks>
/// </remarks>
/// <returns> /// <returns>
/// IdentityResult /// IdentityResult
/// </returns> /// </returns>
@ -149,9 +146,6 @@ public interface IUserService
/// <summary> /// <summary>
/// Gets the organizations that manage the user. /// Gets the organizations that manage the user.
/// </summary> /// </summary>
/// <returns>
/// An empty collection if the Account Deprovisioning feature flag is disabled.
/// </returns>
/// <inheritdoc cref="IsClaimedByAnyOrganizationAsync"/> /// <inheritdoc cref="IsClaimedByAnyOrganizationAsync"/>
Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId); Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId);
} }

View File

@ -301,20 +301,6 @@ public class HandlebarsMailService : IMailService
await EnqueueMailAsync(messageModels); await EnqueueMailAsync(messageModels);
} }
public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
var model = new OrganizationUserRemovedForPolicyTwoStepViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyTwoStep", model);
message.Category = "OrganizationUserRemovedForPolicyTwoStep";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email)
{ {
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
@ -532,20 +518,6 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message); await _mailDeliveryService.SendEmailAsync(message);
} }
public async Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
{
var message = CreateDefaultMessage($"You have been removed from {organizationName}", email);
var model = new OrganizationUserRemovedForPolicySingleOrgViewModel
{
OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName
};
await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicySingleOrg", model);
message.Category = "OrganizationUserRemovedForPolicySingleOrg";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email)
{ {
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
@ -1137,53 +1109,6 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message); await _mailDeliveryService.SendEmailAsync(message);
} }
public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model);
message.Category = "FailedLoginAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
var message = CreateDefaultMessage("Failed login attempts detected", email);
var model = new FailedAuthAttemptsModel()
{
TheDate = utcNow.ToLongDateString(),
TheTime = utcNow.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
IpAddress = ip,
AffectedEmail = email
};
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model);
message.Category = "FailedTwoFactorAttempts";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
var message = CreateDefaultMessage("Domain not verified", adminEmails);
var model = new OrganizationDomainUnverifiedViewModel
{
Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification",
DomainName = domainName
};
await AddMessageContentAsync(message, "OrganizationDomainUnverified", model);
message.Category = "UnverifiedOrganizationDomain";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName) public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{ {
var message = CreateDefaultMessage("Domain not claimed", adminEmails); var message = CreateDefaultMessage("Domain not claimed", adminEmails);

View File

@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Models.Sales;
@ -77,6 +78,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IPremiumUserBillingService _premiumUserBillingService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IDistributedCache _distributedCache; private readonly IDistributedCache _distributedCache;
public UserService( public UserService(
@ -115,6 +117,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IPremiumUserBillingService premiumUserBillingService, IPremiumUserBillingService premiumUserBillingService,
IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache) IDistributedCache distributedCache)
: base( : base(
store, store,
@ -158,6 +161,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_premiumUserBillingService = premiumUserBillingService; _premiumUserBillingService = premiumUserBillingService;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_distributedCache = distributedCache; _distributedCache = distributedCache;
} }
@ -918,7 +922,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await SaveUserAsync(user); await SaveUserAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa); await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
if (!await TwoFactorIsEnabledAsync(user)) if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
{ {
await CheckPoliciesOnTwoFactorRemovalAsync(user); await CheckPoliciesOnTwoFactorRemovalAsync(user);
} }
@ -1280,48 +1284,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
orgAbility.UsersGetPremium && orgAbility.UsersGetPremium &&
orgAbility.Enabled); orgAbility.Enabled);
} }
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
{
var providers = user.GetTwoFactorProviders();
if (providers == null)
{
return false;
}
foreach (var p in providers)
{
if (p.Value?.Enabled ?? false)
{
if (!TwoFactorProvider.RequiresPremium(p.Key))
{
return true;
}
if (await CanAccessPremium(user))
{
return true;
}
}
}
return false;
}
public async Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user)
{
var providers = user.GetTwoFactorProviders();
if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled)
{
return false;
}
if (!TwoFactorProvider.RequiresPremium(provider))
{
return true;
}
return await CanAccessPremium(user);
}
public async Task<string> GenerateSignInTokenAsync(User user, string purpose) public async Task<string> GenerateSignInTokenAsync(User user, string purpose)
{ {
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
@ -1374,11 +1336,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public async Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId) public async Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return Enumerable.Empty<Organization>();
}
// Get all organizations that have verified the user's email domain. // Get all organizations that have verified the user's email domain.
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
@ -1443,22 +1400,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
var removeOrgUserTasks = twoFactorPolicies.Select(async p => var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{ {
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
{ new RevokeOrganizationUsersRequest(
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( p.OrganizationId,
new RevokeOrganizationUsersRequest( [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
p.OrganizationId, new SystemUser(EventSystemUser.TwoFactorDisabled)));
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
new SystemUser(EventSystemUser.TwoFactorDisabled)));
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
}
else
{
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.DisplayName(), user.Email);
}
}).ToArray(); }).ToArray();
await Task.WhenAll(removeOrgUserTasks); await Task.WhenAll(removeOrgUserTasks);

View File

@ -80,11 +80,6 @@ public class NoopMailService : IMailService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
{
return Task.FromResult(0);
}
public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) => public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) =>
Task.CompletedTask; Task.CompletedTask;
@ -155,11 +150,6 @@ public class NoopMailService : IMailService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
{
return Task.FromResult(0);
}
public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
{ {
return Task.FromResult(0); return Task.FromResult(0);
@ -268,21 +258,6 @@ public class NoopMailService : IMailService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
{
return Task.FromResult(0);
}
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{
return Task.FromResult(0);
}
public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName) public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
{ {
return Task.FromResult(0); return Task.FromResult(0);

View File

@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings
public virtual bool EnableCloudCommunication { get; set; } = false; public virtual bool EnableCloudCommunication { get; set; } = false;
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
public virtual string EventGridKey { get; set; } public virtual string EventGridKey { get; set; }
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
public virtual string DatabaseProvider { get; set; } public virtual string DatabaseProvider { get; set; }
@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings
public bool EnforceSsoPolicyForAllUsers { get; set; } public bool EnforceSsoPolicyForAllUsers { get; set; }
} }
public class CaptchaSettings
{
public bool ForceCaptchaRequired { get; set; } = false;
public string HCaptchaSecretKey { get; set; }
public string HCaptchaSiteKey { get; set; }
public int MaximumFailedLoginAttempts { get; set; }
public double MaybeBotScoreThreshold { get; set; } = double.MaxValue;
public double IsBotScoreThreshold { get; set; } = double.MaxValue;
}
public class StripeSettings public class StripeSettings
{ {
public string ApiKey { get; set; } public string ApiKey { get; set; }

View File

@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
cipher.Value.ViewPassword = true; cipher.Value.ViewPassword = true;
} }
} }
else if (await CanAccessUnassignedCiphersAsync(org)) else if (CanAccessUnassignedCiphers(org))
{ {
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId); var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
foreach (var unassignedCipher in unassignedCiphers) foreach (var unassignedCipher in unassignedCiphers)
@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
return false; return false;
} }
private async Task<bool> CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) private bool CanAccessUnassignedCiphers(CurrentContextOrganization org)
{ {
if (org is if (org is
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or

View File

@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
namespace Bit.Core.Vault.Repositories; namespace Bit.Core.Vault.Repositories;

View File

@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
using Bit.Core.Context; using Bit.Core.Context;
@ -37,7 +36,6 @@ public class AccountsController : Controller
private readonly ILogger<AccountsController> _logger; private readonly ILogger<AccountsController> _logger;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IRegisterUserCommand _registerUserCommand; private readonly IRegisterUserCommand _registerUserCommand;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector; private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
@ -85,7 +83,6 @@ public class AccountsController : Controller
ILogger<AccountsController> logger, ILogger<AccountsController> logger,
IUserRepository userRepository, IUserRepository userRepository,
IRegisterUserCommand registerUserCommand, IRegisterUserCommand registerUserCommand,
ICaptchaValidationService captchaValidationService,
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector, IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand, IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
@ -99,7 +96,6 @@ public class AccountsController : Controller
_logger = logger; _logger = logger;
_userRepository = userRepository; _userRepository = userRepository;
_registerUserCommand = registerUserCommand; _registerUserCommand = registerUserCommand;
_captchaValidationService = captchaValidationService;
_assertionOptionsDataProtector = assertionOptionsDataProtector; _assertionOptionsDataProtector = assertionOptionsDataProtector;
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand; _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
@ -167,7 +163,7 @@ public class AccountsController : Controller
} }
[HttpPost("register/finish")] [HttpPost("register/finish")]
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model) public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
{ {
var user = model.ToUser(); var user = model.ToUser();
@ -208,12 +204,11 @@ public class AccountsController : Controller
} }
} }
private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user) private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user)
{ {
if (result.Succeeded) if (result.Succeeded)
{ {
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); return new RegisterFinishResponseModel();
return new RegisterResponseModel(captchaBypassToken);
} }
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))

View File

@ -1,5 +1,4 @@
using Bit.Core.Auth.Models.Business; using Bit.Core.Entities;
using Bit.Core.Entities;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer;
@ -9,7 +8,7 @@ public class CustomValidatorRequestContext
public User User { get; set; } public User User { get; set; }
/// <summary> /// <summary>
/// This is the device that the user is using to authenticate. It can be either known or unknown. /// This is the device that the user is using to authenticate. It can be either known or unknown.
/// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required. /// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation.
/// The option to set it here saves a trip to the database. /// The option to set it here saves a trip to the database.
/// </summary> /// </summary>
public Device Device { get; set; } public Device Device { get; set; }
@ -39,5 +38,4 @@ public class CustomValidatorRequestContext
/// This will be null if the authentication request is successful. /// This will be null if the authentication request is successful.
/// </summary> /// </summary>
public Dictionary<string, object> CustomResponse { get; set; } public Dictionary<string, object> CustomResponse { get; set; }
public CaptchaResponse CaptchaResponse { get; set; }
} }

View File

@ -29,7 +29,6 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IDeviceValidator _deviceValidator; private readonly IDeviceValidator _deviceValidator;
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
@ -49,7 +48,6 @@ public abstract class BaseRequestValidator<T> where T : class
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger logger, ILogger logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -66,7 +64,6 @@ public abstract class BaseRequestValidator<T> where T : class
_deviceValidator = deviceValidator; _deviceValidator = deviceValidator;
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_mailService = mailService;
_logger = logger; _logger = logger;
CurrentContext = currentContext; CurrentContext = currentContext;
_globalSettings = globalSettings; _globalSettings = globalSettings;
@ -81,23 +78,12 @@ public abstract class BaseRequestValidator<T> where T : class
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
CustomValidatorRequestContext validatorContext) CustomValidatorRequestContext validatorContext)
{ {
// 1. We need to check if the user is a bot and if their master password hash is correct. // 1. We need to check if the user's master password hash is correct.
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
var valid = await ValidateContextAsync(context, validatorContext); var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User; var user = validatorContext.User;
if (!valid || isBot) if (!valid)
{ {
if (isBot) await UpdateFailedAuthDetailsAsync(user);
{
_logger.LogInformation(Constants.BypassFiltersEventId,
"Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.",
request.UserName, validatorContext.CaptchaResponse.Score);
}
if (!valid)
{
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
}
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
return; return;
@ -167,7 +153,7 @@ public abstract class BaseRequestValidator<T> where T : class
} }
else else
{ {
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); await UpdateFailedAuthDetailsAsync(user);
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
} }
return; return;
@ -379,7 +365,7 @@ public abstract class BaseRequestValidator<T> where T : class
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
} }
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) private async Task UpdateFailedAuthDetailsAsync(User user)
{ {
if (user == null) if (user == null)
{ {
@ -390,32 +376,6 @@ public abstract class BaseRequestValidator<T> where T : class
user.FailedLoginCount = ++user.FailedLoginCount; user.FailedLoginCount = ++user.FailedLoginCount;
user.LastFailedLoginDate = user.RevisionDate = utcNow; user.LastFailedLoginDate = user.RevisionDate = utcNow;
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
{
if (twoFactorInvalid)
{
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
}
else
{
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
}
}
}
/// <summary>
/// checks to see if a user is trying to log into a new device
/// and has reached the maximum number of failed login attempts.
/// </summary>
/// <param name="unknownDevice">boolean</param>
/// <param name="user">current user</param>
/// <returns></returns>
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
{
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
var failedLoginCount = user?.FailedLoginCount ?? 0;
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
} }
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user) private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)

View File

@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<CustomTokenRequestValidator> logger, ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
deviceValidator, deviceValidator,
twoFactorAuthenticationValidator, twoFactorAuthenticationValidator,
organizationUserRepository, organizationUserRepository,
mailService,
logger, logger,
currentContext, currentContext,
globalSettings, globalSettings,

View File

@ -22,8 +22,7 @@ public class DeviceValidator(
ICurrentContext currentContext, ICurrentContext currentContext,
IUserService userService, IUserService userService,
IDistributedCache distributedCache, IDistributedCache distributedCache,
ILogger<DeviceValidator> logger, ILogger<DeviceValidator> logger) : IDeviceValidator
IFeatureService featureService) : IDeviceValidator
{ {
private readonly IDeviceService _deviceService = deviceService; private readonly IDeviceService _deviceService = deviceService;
private readonly IDeviceRepository _deviceRepository = deviceRepository; private readonly IDeviceRepository _deviceRepository = deviceRepository;
@ -33,7 +32,6 @@ public class DeviceValidator(
private readonly IUserService _userService = userService; private readonly IUserService _userService = userService;
private readonly IDistributedCache distributedCache = distributedCache; private readonly IDistributedCache distributedCache = distributedCache;
private readonly ILogger<DeviceValidator> _logger = logger; private readonly ILogger<DeviceValidator> _logger = logger;
private readonly IFeatureService _featureService = featureService;
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
{ {
@ -64,9 +62,7 @@ public class DeviceValidator(
} }
// We have established that the device is unknown at this point; begin new device verification // We have established that the device is unknown at this point; begin new device verification
// PM-13340: remove feature flag if (request.GrantType == "password" &&
if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) &&
request.GrantType == "password" &&
request.Raw["AuthRequest"] == null && request.Raw["AuthRequest"] == null &&
!context.TwoFactorRequired && !context.TwoFactorRequired &&
!context.SsoRequired && !context.SsoRequired &&

View File

@ -0,0 +1,38 @@

using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Entities;
using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer.RequestValidators;
public interface ITwoFactorAuthenticationValidator
{
/// <summary>
/// Check if the user is required to use two-factor authentication to login. This is based on the user's
/// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type.
/// Client credentials and webauthn grant types do not require two-factor authentication.
/// </summary>
/// <param name="user">the active user for the request</param>
/// <param name="request">the request that contains the grant types</param>
/// <returns>boolean</returns>
Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);
/// <summary>
/// Builds the two-factor authentication result for the user based on the available two-factor providers
/// from either their user account or Organization.
/// </summary>
/// <param name="user">user trying to login</param>
/// <param name="organization">organization associated with the user; Can be null</param>
/// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>
Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);
/// <summary>
/// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses
/// organization duo, it will use the organization duo token provider to verify the token.
/// </summary>
/// <param name="user">the active User</param>
/// <param name="organization">organization of user; can be null</param>
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
/// <returns>boolean</returns>
Task<bool> VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
}

View File

@ -3,7 +3,6 @@ using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -21,7 +20,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
{ {
private UserManager<User> _userManager; private UserManager<User> _userManager;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IAuthRequestRepository _authRequestRepository; private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDeviceValidator _deviceValidator; private readonly IDeviceValidator _deviceValidator;
public ResourceOwnerPasswordValidator( public ResourceOwnerPasswordValidator(
@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<ResourceOwnerPasswordValidator> logger, ILogger<ResourceOwnerPasswordValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository, IAuthRequestRepository authRequestRepository,
IUserRepository userRepository, IUserRepository userRepository,
IPolicyService policyService, IPolicyService policyService,
@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
deviceValidator, deviceValidator,
twoFactorAuthenticationValidator, twoFactorAuthenticationValidator,
organizationUserRepository, organizationUserRepository,
mailService,
logger, logger,
currentContext, currentContext,
globalSettings, globalSettings,
@ -63,7 +58,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
{ {
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;
_captchaValidationService = captchaValidationService;
_authRequestRepository = authRequestRepository; _authRequestRepository = authRequestRepository;
_deviceValidator = deviceValidator; _deviceValidator = deviceValidator;
} }
@ -88,37 +82,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
Device = knownDevice ?? requestDevice, Device = knownDevice ?? requestDevice,
}; };
string bypassToken = null;
if (!validatorContext.KnownDevice &&
_captchaValidationService.RequireCaptchaValidation(_currentContext, user))
{
var captchaResponse = context.Request.Raw["captchaResponse"]?.ToString();
if (string.IsNullOrWhiteSpace(captchaResponse))
{
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Captcha required.",
new Dictionary<string, object>
{
{ _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey },
});
return;
}
validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync(
captchaResponse, _currentContext.IpAddress, user);
if (!validatorContext.CaptchaResponse.Success)
{
await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null);
return;
}
bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
}
await ValidateAsync(context, context.Request, validatorContext); await ValidateAsync(context, context.Request, validatorContext);
if (context.Result.CustomResponse != null && bypassToken != null)
{
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
}
} }
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context, protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,

View File

@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
@ -16,56 +17,25 @@ using Microsoft.AspNetCore.Identity;
namespace Bit.Identity.IdentityServer.RequestValidators; namespace Bit.Identity.IdentityServer.RequestValidators;
public interface ITwoFactorAuthenticationValidator
{
/// <summary>
/// Check if the user is required to use two-factor authentication to login. This is based on the user's
/// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type.
/// Client credentials and webauthn grant types do not require two-factor authentication.
/// </summary>
/// <param name="user">the active user for the request</param>
/// <param name="request">the request that contains the grant types</param>
/// <returns>boolean</returns>
Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);
/// <summary>
/// Builds the two-factor authentication result for the user based on the available two-factor providers
/// from either their user account or Organization.
/// </summary>
/// <param name="user">user trying to login</param>
/// <param name="organization">organization associated with the user; Can be null</param>
/// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>
Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);
/// <summary>
/// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses
/// organization duo, it will use the organization duo token provider to verify the token.
/// </summary>
/// <param name="user">the active User</param>
/// <param name="organization">organization of user; can be null</param>
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
/// <returns>boolean</returns>
Task<bool> VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
}
public class TwoFactorAuthenticationValidator( public class TwoFactorAuthenticationValidator(
IUserService userService, IUserService userService,
UserManager<User> userManager, UserManager<User> userManager,
IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider,
IFeatureService featureService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory, IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
ICurrentContext currentContext) : ITwoFactorAuthenticationValidator ICurrentContext currentContext) : ITwoFactorAuthenticationValidator
{ {
private readonly IUserService _userService = userService; private readonly IUserService _userService = userService;
private readonly UserManager<User> _userManager = userManager; private readonly UserManager<User> _userManager = userManager;
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;
private readonly IFeatureService _featureService = featureService;
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
private readonly IOrganizationRepository _organizationRepository = organizationRepository; private readonly IOrganizationRepository _organizationRepository = organizationRepository;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
private readonly ICurrentContext _currentContext = currentContext; private readonly ICurrentContext _currentContext = currentContext;
public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
@ -121,7 +91,10 @@ public class TwoFactorAuthenticationValidator(
{ "TwoFactorProviders2", providers }, { "TwoFactorProviders2", providers },
}; };
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token // If we have an Email 2FA provider we need this session token so SSO users
// can re-request an email TOTP. The TwoFactorController.SendEmailLoginAsync
// endpoint requires a way to authenticate the user before sending another email with
// a TOTP, this token acts as the authentication mechanism.
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
{ {
twoFactorResultDict.Add("SsoEmail2faSessionToken", twoFactorResultDict.Add("SsoEmail2faSessionToken",
@ -130,12 +103,6 @@ public class TwoFactorAuthenticationValidator(
twoFactorResultDict.Add("Email", user.Email); twoFactorResultDict.Add("Email", user.Email);
} }
if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
{
// Send email now if this is their only 2FA method
await _userService.SendTwoFactorEmailAsync(user);
}
return twoFactorResultDict; return twoFactorResultDict;
} }
@ -161,7 +128,7 @@ public class TwoFactorAuthenticationValidator(
// These cases we want to always return false, U2f is deprecated and OrganizationDuo // These cases we want to always return false, U2f is deprecated and OrganizationDuo
// uses a different flow than the other two factor providers, it follows the same // uses a different flow than the other two factor providers, it follows the same
// structure of a UserTokenProvider but has it's logic ran outside the usual token // structure of a UserTokenProvider but has it's logic runs outside the usual token
// provider flow. See IOrganizationDuoUniversalTokenProvider.cs // provider flow. See IOrganizationDuoUniversalTokenProvider.cs
if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo) if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo)
{ {
@ -171,12 +138,12 @@ public class TwoFactorAuthenticationValidator(
// Now we are concerning the rest of the Two Factor Provider Types // Now we are concerning the rest of the Two Factor Provider Types
// The intent of this check is to make sure that the user is using a 2FA provider that // The intent of this check is to make sure that the user is using a 2FA provider that
// is enabled and allowed by their premium status. The exception for Remember // is enabled and allowed by their premium status.
// is because it is a "special" 2FA type that isn't ever explicitly // The exception for Remember is because it is a "special" 2FA type that isn't ever explicitly
// enabled by a user, so we can't check the user's 2FA providers to see if they're // enabled by a user, so we can't check the user's 2FA providers to see if they're
// enabled. We just have to check if the token is valid. // enabled. We just have to check if the token is valid.
if (type != TwoFactorProviderType.Remember && if (type != TwoFactorProviderType.Remember &&
!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) user.GetTwoFactorProvider(type) == null)
{ {
return false; return false;
} }

View File

@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IDeviceValidator deviceValidator, IDeviceValidator deviceValidator,
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
ILogger<CustomTokenRequestValidator> logger, ILogger<CustomTokenRequestValidator> logger,
ICurrentContext currentContext, ICurrentContext currentContext,
GlobalSettings globalSettings, GlobalSettings globalSettings,
@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
deviceValidator, deviceValidator,
twoFactorAuthenticationValidator, twoFactorAuthenticationValidator,
organizationUserRepository, organizationUserRepository,
mailService,
logger, logger,
currentContext, currentContext,
globalSettings, globalSettings,

View File

@ -1,7 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json; using System.Text.Json;
using Bit.Core; using Bit.Core;
using Bit.Core.Auth.Models.Api;
using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -9,7 +8,7 @@ using Bit.Core.Utilities;
namespace Bit.Identity.Models.Request.Accounts; namespace Bit.Identity.Models.Request.Accounts;
public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel public class RegisterRequestModel : IValidatableObject
{ {
[StringLength(50)] [StringLength(50)]
public string Name { get; set; } public string Name { get; set; }
@ -22,7 +21,6 @@ public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
[StringLength(50)] [StringLength(50)]
public string MasterPasswordHint { get; set; } public string MasterPasswordHint { get; set; }
public string CaptchaResponse { get; set; }
public string Key { get; set; } public string Key { get; set; }
public KeysRequestModel Keys { get; set; } public KeysRequestModel Keys { get; set; }
public string Token { get; set; } public string Token { get; set; }

View File

@ -1,5 +0,0 @@
namespace Bit.Identity.Models.Response.Accounts;
public interface ICaptchaProtectedResponseModel
{
public string CaptchaBypassToken { get; set; }
}

View File

@ -0,0 +1,10 @@
using Bit.Core.Models.Api;
namespace Bit.Identity.Models.Response.Accounts;
public class RegisterFinishResponseModel : ResponseModel
{
public RegisterFinishResponseModel()
: base("registerFinish")
{ }
}

View File

@ -1,14 +0,0 @@
using Bit.Core.Models.Api;
namespace Bit.Identity.Models.Response.Accounts;
public class RegisterResponseModel : ResponseModel, ICaptchaProtectedResponseModel
{
public RegisterResponseModel(string captchaBypassToken)
: base("register")
{
CaptchaBypassToken = captchaBypassToken;
}
public string CaptchaBypassToken { get; set; }
}

View File

@ -17,9 +17,6 @@
}, },
"braintree": { "braintree": {
"production": true "production": true
},
"captcha": {
"maximumFailedLoginAttempts": 5
} }
}, },
"Logging": { "Logging": {

View File

@ -14,9 +14,6 @@
"internalVault": null, "internalVault": null,
"internalSso": null, "internalSso": null,
"internalScim": null "internalScim": null
},
"captcha": {
"maximumFailedLoginAttempts": 0
} }
} }
} }

View File

@ -1,10 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" /> <ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup> </ItemGroup>

View File

@ -253,7 +253,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
} }
} }
public async Task UpdateUserKeyAndEncryptedDataV2Async( public async Task UpdateUserKeyAndEncryptedDataV2Async(
User user, User user,
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions) IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)
@ -289,7 +288,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
UnprotectData(user); UnprotectData(user);
} }
public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids) public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
{ {
using (var connection = new SqlConnection(ReadOnlyConnectionString)) using (var connection = new SqlConnection(ReadOnlyConnectionString))
@ -318,6 +316,14 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
} }
} }
public async Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid userId)
{
var result = await GetManyWithCalculatedPremiumAsync([userId]);
UnprotectData(result);
return result.SingleOrDefault();
}
private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask) private async Task ProtectDataAndSaveAsync(User user, Func<Task> saveTask)
{ {
if (user == null) if (user == null)

View File

@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
public IQueryable<OrganizationUserOrganizationDetails> Run(DatabaseContext dbContext) public IQueryable<OrganizationUserOrganizationDetails> Run(DatabaseContext dbContext)
{ {
var query = from ou in dbContext.OrganizationUsers var query = from ou in dbContext.OrganizationUsers
join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization join o in dbContext.Organizations on ou.OrganizationId equals o.Id
from o in outerOrganization.DefaultIfEmpty()
join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g
from su in su_g.DefaultIfEmpty() from su in su_g.DefaultIfEmpty()
join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g
@ -68,10 +67,11 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
SmServiceAccounts = o.SmServiceAccounts, SmServiceAccounts = o.SmServiceAccounts,
LimitCollectionCreation = o.LimitCollectionCreation, LimitCollectionCreation = o.LimitCollectionCreation,
LimitCollectionDeletion = o.LimitCollectionDeletion, LimitCollectionDeletion = o.LimitCollectionDeletion,
LimitItemDeletion = o.LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
UseRiskInsights = o.UseRiskInsights, UseRiskInsights = o.UseRiskInsights,
UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies, UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies,
LimitItemDeletion = o.LimitItemDeletion,
IsAdminInitiated = os.IsAdminInitiated
}; };
return query; return query;
} }

View File

@ -1,10 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0108;CS8632</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="linq2db" Version="5.4.1" /> <PackageReference Include="linq2db" Version="5.4.1" />

View File

@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations;
namespace Bit.Infrastructure.EntityFramework.Platform; namespace Bit.Infrastructure.EntityFramework.Platform;
public class Installation : C.Installation public class Installation : C.Installation;
{
// Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129
// This isn't a value or entity used by self hosted servers, but it's
// being added for synchronicity between database provider options.
public DateTime? LastActivityDate { get; set; }
}
public class InstallationMapperProfile : Profile public class InstallationMapperProfile : Profile
{ {
public InstallationMapperProfile() public InstallationMapperProfile()
{ {
CreateMap<C.Installation, Installation>()
// Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129
.ForMember(i => i.LastActivityDate, opt => opt.Ignore())
.ReverseMap();
CreateMap<C.Installation, Installation>().ReverseMap(); CreateMap<C.Installation, Installation>().ReverseMap();
} }
} }

View File

@ -1,10 +1,10 @@
using AutoMapper; using AutoMapper;
using Bit.Core.KeyManagement.UserKey; using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using DataModel = Bit.Core.Models.Data;
#nullable enable #nullable enable
@ -38,13 +38,13 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
} }
} }
public async Task<DataModel.UserKdfInformation?> GetKdfInformationByEmailAsync(string email) public async Task<UserKdfInformation?> GetKdfInformationByEmailAsync(string email)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
return await GetDbSet(dbContext).Where(e => e.Email == email) return await GetDbSet(dbContext).Where(e => e.Email == email)
.Select(e => new DataModel.UserKdfInformation .Select(e => new UserKdfInformation
{ {
Kdf = e.Kdf, Kdf = e.Kdf,
KdfIterations = e.KdfIterations, KdfIterations = e.KdfIterations,
@ -251,13 +251,13 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
} }
} }
public async Task<IEnumerable<DataModel.UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids) public async Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())
{ {
var dbContext = GetDatabaseContext(scope); var dbContext = GetDatabaseContext(scope);
var users = dbContext.Users.Where(x => ids.Contains(x.Id)); var users = dbContext.Users.Where(x => ids.Contains(x.Id));
return await users.Select(e => new DataModel.UserWithCalculatedPremium(e) return await users.Select(e => new UserWithCalculatedPremium(e)
{ {
HasPremiumAccess = e.Premium || dbContext.OrganizationUsers HasPremiumAccess = e.Premium || dbContext.OrganizationUsers
.Any(ou => ou.UserId == e.Id && .Any(ou => ou.UserId == e.Id &&
@ -269,6 +269,12 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
} }
} }
public async Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid id)
{
var result = await GetManyWithCalculatedPremiumAsync([id]);
return result.FirstOrDefault();
}
public override async Task DeleteAsync(Core.Entities.User user) public override async Task DeleteAsync(Core.Entities.User user)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using (var scope = ServiceScopeFactory.CreateScope())

View File

@ -151,14 +151,6 @@ public static class ServiceCollectionExtensions
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>()) serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>())
); );
services.AddSingleton<IDataProtectorTokenFactory<HCaptchaTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<HCaptchaTokenable>(
HCaptchaTokenable.ClearTextPrefix,
HCaptchaTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<HCaptchaTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider => services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<SsoTokenable>( new DataProtectorTokenFactory<SsoTokenable>(
SsoTokenable.ClearTextPrefix, SsoTokenable.ClearTextPrefix,
@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions
{ {
services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>(); services.AddSingleton<IReferenceEventService, AzureQueueReferenceEventService>();
} }
if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) &&
CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey))
{
services.AddSingleton<ICaptchaValidationService, HCaptchaValidationService>();
}
else
{
services.AddSingleton<ICaptchaValidationService, NoopCaptchaValidationService>();
}
} }
public static void AddOosServices(this IServiceCollection services) public static void AddOosServices(this IServiceCollection services)

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build"> <Project DefaultTargets="Build">
<Sdk Name="Microsoft.Build.Sql" Version="0.1.9-preview" /> <Sdk Name="Microsoft.Build.Sql"/>
<PropertyGroup> <PropertyGroup>
<Name>Sql</Name> <Name>Sql</Name>
<ProjectGuid>{58554e52-fdec-4832-aff9-302b01e08dca}</ProjectGuid> <ProjectGuid>{58554e52-fdec-4832-aff9-302b01e08dca}</ProjectGuid>

View File

@ -3,9 +3,9 @@ CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail]
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
DECLARE @Domain NVARCHAR(256) DECLARE @Domain NVARCHAR(256)
SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))
SELECT SELECT
@ -19,8 +19,8 @@ BEGIN
[dbo].[OrganizationView] O [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD INNER JOIN [dbo].[OrganizationDomainView] OD
ON O.Id = OD.OrganizationId ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S LEFT JOIN [dbo].[SsoConfig] S
ON O.Id = S.OrganizationId ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain WHERE OD.DomainName = @Domain
AND O.Enabled = 1 AND O.Enabled = 1
END END

View File

@ -1,14 +0,0 @@
CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]
@SponsoringOrganizationUserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OrganizationSponsorshipView]
WHERE
[SponsoringOrganizationUserId] = @SponsoringOrganizationUserId
END
GO

View File

@ -0,0 +1,15 @@
CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]
@SponsoringOrganizationUserId UNIQUEIDENTIFIER,
@IsAdminInitiated BIT = 0
AS
BEGIN
SET NOCOUNT ON;
SELECT
*
FROM
[dbo].[OrganizationSponsorshipView]
WHERE
[SponsoringOrganizationUserID] = @SponsoringOrganizationUserId
and [IsAdminInitiated] = @IsAdminInitiated
END

View File

@ -15,7 +15,7 @@ BEGIN
OD.DomainName OD.DomainName
FROM [dbo].[OrganizationView] O FROM [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain WHERE OD.DomainName = @Domain
AND O.Enabled = 1 AND O.Enabled = 1
AND OD.VerifiedDate IS NOT NULL AND OD.VerifiedDate IS NOT NULL

View File

@ -51,7 +51,8 @@ SELECT
O.[AllowAdminAccessToAllCollectionItems], O.[AllowAdminAccessToAllCollectionItems],
O.[UseRiskInsights], O.[UseRiskInsights],
O.[UseAdminSponsoredFamilies], O.[UseAdminSponsoredFamilies],
O.[LimitItemDeletion] O.[LimitItemDeletion],
OS.[IsAdminInitiated]
FROM FROM
[dbo].[OrganizationUser] OU [dbo].[OrganizationUser] OU
LEFT JOIN LEFT JOIN

View File

@ -0,0 +1,39 @@
using System.Net;
using System.Net.Http.Headers;
using Bit.Api.IntegrationTest.Factories;
using Bit.Seeder.Recipes;
using Xunit;
using Xunit.Abstractions;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper)
{
[Theory(Skip = "Performance test")]
[InlineData(100)]
[InlineData(60000)]
public async Task GetAsync(int seats)
{
await using var factory = new ApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var seeder = new OrganizationWithUsersRecipe(db);
var orgId = seeder.Seed("Org", seats, "large.test");
var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(result);
stopwatch.Stop();
testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms");
}
}

View File

@ -18,6 +18,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Api\Api.csproj" /> <ProjectReference Include="..\..\src\Api\Api.csproj" />
<ProjectReference Include="..\..\util\Seeder\Seeder.csproj" />
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" /> <ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
<Content Include="..\..\src\Api\appsettings.*.json"> <Content Include="..\..\src\Api\appsettings.*.json">

View File

@ -238,20 +238,13 @@ public class OrganizationUsersControllerTests
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model)); await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
} }
[Theory] [Theory, BitAutoData]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task Get_ReturnsUser( public async Task Get_ReturnsUser(
bool accountDeprovisioningEnabled,
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections, OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
SutProvider<OrganizationUsersController> sutProvider) SutProvider<OrganizationUsersController> sutProvider)
{ {
organizationUser.Permissions = null; organizationUser.Permissions = null;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(accountDeprovisioningEnabled);
sutProvider.GetDependency<ICurrentContext>() sutProvider.GetDependency<ICurrentContext>()
.ManageUsers(organizationUser.OrganizationId) .ManageUsers(organizationUser.OrganizationId)
.Returns(true); .Returns(true);
@ -267,8 +260,8 @@ public class OrganizationUsersControllerTests
var response = await sutProvider.Sut.Get(organizationUser.Id, false); var response = await sutProvider.Sut.Get(organizationUser.Id, false);
Assert.Equal(organizationUser.Id, response.Id); Assert.Equal(organizationUser.Id, response.Id);
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization); Assert.True(response.ManagedByOrganization);
Assert.Equal(accountDeprovisioningEnabled, response.ClaimedByOrganization); Assert.True(response.ClaimedByOrganization);
} }
[Theory] [Theory]

View File

@ -140,7 +140,6 @@ public class OrganizationsControllerTests : IDisposable
_currentContext.OrganizationUser(orgId).Returns(true); _currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null }); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
@ -170,7 +169,6 @@ public class OrganizationsControllerTests : IDisposable
_currentContext.OrganizationUser(orgId).Returns(true); _currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } }); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } });
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
@ -205,7 +203,6 @@ public class OrganizationsControllerTests : IDisposable
_currentContext.OrganizationUser(orgId).Returns(true); _currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user); _userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>()); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
await _sut.Leave(orgId); await _sut.Leave(orgId);

View File

@ -2,8 +2,6 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<!-- Temp exclusions until warnings are fixed -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8620;CS0169</WarningsNotAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,13 +7,13 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Validators; using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -40,6 +40,7 @@ public class AccountsControllerTests : IDisposable
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
@ -64,6 +65,7 @@ public class AccountsControllerTests : IDisposable
_policyService = Substitute.For<IPolicyService>(); _policyService = Substitute.For<IPolicyService>();
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>(); _setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>(); _rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>(); _tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>(); _featureService = Substitute.For<IFeatureService>();
_cipherValidator = _cipherValidator =
@ -87,6 +89,7 @@ public class AccountsControllerTests : IDisposable
_setInitialMasterPasswordCommand, _setInitialMasterPasswordCommand,
_tdeOffboardingPasswordCommand, _tdeOffboardingPasswordCommand,
_rotateUserKeyCommand, _rotateUserKeyCommand,
_twoFactorIsEnabledQuery,
_featureService, _featureService,
_cipherValidator, _cipherValidator,
_folderValidator, _folderValidator,
@ -189,21 +192,6 @@ public class AccountsControllerTests : IDisposable
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
} }
[Fact]
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangeEmailAsync(user, default, default, default, default, default)
.Returns(Task.FromResult(IdentityResult.Success));
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
await _sut.PostEmail(new EmailRequestModel());
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
}
[Fact] [Fact]
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException() public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
{ {
@ -533,12 +521,11 @@ public class AccountsControllerTests : IDisposable
} }
[Fact] [Fact]
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException() public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()
{ {
var user = GenerateExampleUser(); var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user); ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel())); var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
@ -547,12 +534,11 @@ public class AccountsControllerTests : IDisposable
} }
[Fact] [Fact]
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed() public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
{ {
var user = GenerateExampleUser(); var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user); ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
_userService.DeleteAsync(user).Returns(IdentityResult.Success); _userService.DeleteAsync(user).Returns(IdentityResult.Success);

View File

@ -216,6 +216,12 @@ public class OrganizationSponsorshipsControllerTests
sutProvider.GetDependency<IOrganizationSponsorshipRepository>() sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships); .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships);
// Set IsAdminInitiated to true for all test sponsorships
foreach (var sponsorship in sponsorships)
{
sponsorship.IsAdminInitiated = true;
}
// Act // Act
var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id); var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id);

Some files were not shown because too many files have changed in this diff Show More