mirror of
https://github.com/bitwarden/server.git
synced 2025-05-25 21:34:52 -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:
commit
eac9f13100
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -377,6 +389,8 @@ Global
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {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}
|
||||
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -4,6 +4,7 @@
|
||||
"rollForward": "latestFeature"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "4.1.0"
|
||||
"Microsoft.Build.Traversal": "4.1.0",
|
||||
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -89,7 +88,7 @@ public class UsersController : Controller
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@ -106,7 +105,7 @@ public class UsersController : Controller
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(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);
|
||||
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
@ -167,7 +166,6 @@ public class UsersController : Controller
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
@ -179,12 +177,4 @@ public class UsersController : Controller
|
||||
await _userService.ToggleNewDeviceVerificationException(user.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;
|
||||
}
|
||||
}
|
||||
|
@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller
|
||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
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)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||
}
|
||||
|
||||
var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds);
|
||||
return usersOrganizationClaimedStatus;
|
||||
}
|
||||
|
@ -279,8 +279,7 @@ public class OrganizationsController : Controller
|
||||
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id))
|
||||
if ((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.");
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@ -79,7 +78,7 @@ public class PoliciesController : Controller
|
||||
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);
|
||||
}
|
||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
@ -135,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
/// Obsolete.
|
||||
///
|
||||
/// See <see cref="UserIsClaimedByOrganization"/>
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
/// <summary>
|
||||
/// Indicates if the organization claims the user.
|
||||
/// Indicates if the user is claimed by the organization.
|
||||
/// </summary>
|
||||
/// <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.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
public bool UserIsClaimedByOrganization { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||
collections);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
||||
{
|
||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||
response = new MemberResponseModel(existingUserDetails,
|
||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -4,8 +4,6 @@
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||
ProviderUserStatusType.Confirmed);
|
||||
|
||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
||||
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
||||
}
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
@ -514,9 +518,8 @@ public class AccountsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
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]
|
||||
[HttpPost("resend-new-device-otp")]
|
||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Route("accounts")]
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService) : Controller
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
@ -56,7 +58,7 @@ public class AccountsController(
|
||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||
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 organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
@ -292,6 +293,7 @@ public class OrganizationBillingController(
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
await organizationBillingService.Finalize(sale);
|
||||
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)
|
||||
{
|
||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||
|
@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
|
||||
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
|
||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||
sponsorships
|
||||
.Where(s => s.IsAdminInitiated)
|
||||
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
@ -1086,9 +1086,8 @@ public class CiphersController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
// Check if the user is claimed by any organization.
|
||||
if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
||||
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 organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand(
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ICurrentContext currentContext,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IMailService mailService,
|
||||
@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand(
|
||||
|
||||
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) =>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
|
||||
public AcceptOrgUserCommand(
|
||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
IPolicyService policyService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||
{
|
||||
|
||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
_policyService = policyService;
|
||||
_mailService = mailService;
|
||||
_userRepository = userRepository;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
}
|
||||
|
||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
||||
}
|
||||
|
||||
// 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,
|
||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||
|
@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
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 });
|
||||
if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed)
|
||||
@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
||||
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))
|
||||
: filteredUsers.ToDictionary(u => u.Id, u => false);
|
||||
var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>();
|
||||
|
@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
{
|
||||
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);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
_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)
|
||||
{
|
||||
if (policyUpdate is not { Enabled: true })
|
||||
@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator
|
||||
return validateDecryptionErrorMessage;
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId))
|
||||
{
|
||||
return ClaimedDomainSingleOrganizationRequiredErrorMessage;
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
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.";
|
||||
@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICurrentContext currentContext,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IFeatureService featureService,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_organizationRepository = organizationRepository;
|
||||
_currentContext = currentContext;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_featureService = featureService;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
{
|
||||
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);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
else
|
||||
{
|
||||
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
|
||||
}
|
||||
var currentUser = _currentContext.UserId ?? Guid.Empty;
|
||||
var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId);
|
||||
await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider));
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
|
||||
_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(
|
||||
IEnumerable<OrganizationUserUserDetails> orgUserDetails,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) =>
|
||||
|
@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
//Send email to administrators
|
||||
if (adminEmails.Count > 0)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails,
|
||||
domain.OrganizationId.ToString(), domain.DomainName);
|
||||
}
|
||||
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName);
|
||||
|
@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte
|
||||
Email = 1,
|
||||
Duo = 2,
|
||||
YubiKey = 3,
|
||||
U2f = 4, // Deprecated
|
||||
[Obsolete("Deprecated in favor of WebAuthn.")]
|
||||
U2f = 4,
|
||||
Remember = 5,
|
||||
OrganizationDuo = 6,
|
||||
WebAuthn = 7,
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}";
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;
|
||||
|
||||
public AuthenticatorTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
[FromKeyedServices("persistent")]
|
||||
IDistributedCache distributedCache)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_distributedCache = distributedCache;
|
||||
_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);
|
||||
if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"]))
|
||||
var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
|
||||
if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"]))
|
||||
{
|
||||
return false;
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
return await _serviceProvider.GetRequiredService<IUserService>()
|
||||
.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user);
|
||||
return Task.FromResult(authenticatorProvider.Enabled);
|
||||
}
|
||||
|
||||
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
|
@ -17,7 +17,7 @@ public class DuoUniversalTokenProvider(
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the
|
||||
/// problem.
|
||||
@ -29,12 +29,13 @@ public class DuoUniversalTokenProvider(
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (provider == null)
|
||||
var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService);
|
||||
if (duoUniversalTokenProvider == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
||||
|
||||
return duoUniversalTokenProvider.Enabled;
|
||||
}
|
||||
|
||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
@ -58,7 +59,7 @@ public class DuoUniversalTokenProvider(
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="user">Active User</param>
|
||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||
|
@ -1,7 +1,6 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders;
|
||||
|
||||
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public EmailTwoFactorTokenProvider(
|
||||
IServiceProvider serviceProvider,
|
||||
[FromKeyedServices("persistent")]
|
||||
IDistributedCache distributedCache) :
|
||||
base(distributedCache)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
|
||||
TokenAlpha = false;
|
||||
TokenNumeric = true;
|
||||
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);
|
||||
if (!HasProperMetaData(provider))
|
||||
var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||
if (!HasProperMetaData(emailTokenProvider))
|
||||
{
|
||||
return false;
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
return await _serviceProvider.GetRequiredService<IUserService>().
|
||||
TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user);
|
||||
return Task.FromResult(emailTokenProvider.Enabled);
|
||||
}
|
||||
|
||||
public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
|
@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
_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);
|
||||
// null check happens in this method
|
||||
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)
|
||||
@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||
var keys = LoadKeys(provider);
|
||||
|
||||
if (!provider.MetaData.ContainsKey("login"))
|
||||
if (!provider.MetaData.TryGetValue("login", out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
var jsonOptions = provider.MetaData["login"].ToString();
|
||||
var jsonOptions = value.ToString();
|
||||
var options = AssertionOptions.FromJson(jsonOptions);
|
||||
|
||||
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)
|
||||
{
|
||||
return provider?.MetaData?.Any() ?? false;
|
||||
|
@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
|
||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||
{
|
||||
// Ensure the user has access to premium
|
||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||
if (!await userService.CanAccessPremium(user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);
|
||||
if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)
|
||||
// Check if the user has a YubiKey provider configured
|
||||
var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);
|
||||
if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user);
|
||||
return yubicoProvider.Enabled;
|
||||
}
|
||||
|
||||
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||
|
@ -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.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@ -167,7 +167,7 @@ public class UserStore :
|
||||
|
||||
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)
|
||||
|
@ -1,6 +0,0 @@
|
||||
namespace Bit.Core.Auth.Models.Api;
|
||||
|
||||
public interface ICaptchaProtectedModel
|
||||
{
|
||||
string CaptchaResponse { get; set; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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);
|
||||
}
|
@ -4,9 +4,10 @@ using Bit.Core.Tokens;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Business.Tokenables;
|
||||
|
||||
// This token just provides a verifiable authN mechanism for the API service
|
||||
// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be
|
||||
// used maliciously.
|
||||
/// <summary>
|
||||
/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync
|
||||
/// anonymous endpoint so it cannot used maliciously.
|
||||
/// </summary>
|
||||
public class SsoEmail2faSessionTokenable : ExpiringTokenable
|
||||
{
|
||||
// Just over 2 min expiration (client expires session after 2 min)
|
||||
|
@ -1,10 +1,18 @@
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Auth.Models;
|
||||
|
||||
public interface ITwoFactorProvidersUser
|
||||
{
|
||||
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();
|
||||
Guid? GetUserId();
|
||||
bool GetPremium();
|
||||
|
@ -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);
|
||||
}
|
@ -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() { }
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
|
||||
|
||||
public interface ITwoFactorIsEnabledQuery
|
||||
{
|
||||
/// <summary>
|
||||
@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery
|
||||
/// <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;
|
||||
/// <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>
|
||||
/// <param name="user">The user to check.</param>
|
||||
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
||||
|
@ -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.Repositories;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||
|
||||
public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
||||
public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public TwoFactorIsEnabledQuery(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
private readonly IUserRepository _userRepository = userRepository;
|
||||
|
||||
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
|
||||
{
|
||||
@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
||||
return result;
|
||||
}
|
||||
|
||||
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList());
|
||||
|
||||
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]);
|
||||
foreach (var userDetail in userDetails)
|
||||
{
|
||||
var hasTwoFactor = false;
|
||||
var providers = userDetail.GetTwoFactorProviders();
|
||||
if (providers != null)
|
||||
{
|
||||
// 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));
|
||||
result.Add(
|
||||
(userDetail.Id,
|
||||
await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(),
|
||||
() => Task.FromResult(userDetail.HasPremiumAccess))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
||||
return false;
|
||||
}
|
||||
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
if (providers == null || !providers.Any())
|
||||
return await TwoFactorEnabledAsync(
|
||||
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;
|
||||
}
|
||||
|
||||
// Get all enabled providers
|
||||
var enabledProviderKeys = providers
|
||||
.Where(provider => provider.Value?.Enabled ?? false)
|
||||
.Select(provider => provider.Key);
|
||||
// TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.
|
||||
var enabledProviderKeys = from provider in providers
|
||||
where provider.Value?.Enabled ?? false
|
||||
select provider.Key;
|
||||
|
||||
// If no providers are enabled then two factor is not enabled
|
||||
if (!enabledProviderKeys.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine if any enabled provider passes the premium check
|
||||
var hasTwoFactor = enabledProviderKeys
|
||||
.Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type))
|
||||
.FirstOrDefault();
|
||||
|
||||
// If no enabled provider passes the check, check the repository for organization premium access
|
||||
if (!hasTwoFactor)
|
||||
// If there are only premium two factor options then standard two factor is not enabled
|
||||
var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium);
|
||||
if (onlyHasPremiumTwoFactor)
|
||||
{
|
||||
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List<Guid> { userId.Value });
|
||||
var userDetail = userDetails.FirstOrDefault();
|
||||
|
||||
if (userDetail != null)
|
||||
{
|
||||
hasTwoFactor = enabledProviderKeys
|
||||
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
// There are no Standard two factor options, check if the user has premium access
|
||||
// If the user has premium access, then two factor is enabled
|
||||
var premiumAccess = await hasPremiumAccessDelegate();
|
||||
return premiumAccess;
|
||||
}
|
||||
|
||||
return hasTwoFactor;
|
||||
// The user has at least one non-premium two factor option
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ public interface IProviderBillingService
|
||||
int seatAdjustment);
|
||||
|
||||
/// <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:
|
||||
/// <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>
|
||||
|
@ -5,14 +5,12 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
public class PaymentHistoryService(
|
||||
IStripeAdapter stripeAdapter,
|
||||
ITransactionRepository transactionRepository,
|
||||
ILogger<PaymentHistoryService> logger) : IPaymentHistoryService
|
||||
ITransactionRepository transactionRepository) : IPaymentHistoryService
|
||||
{
|
||||
public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
|
||||
ISubscriber subscriber,
|
||||
|
@ -115,7 +115,6 @@ public static class FeatureFlagKeys
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
public const string EmailVerification = "email-verification";
|
||||
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 ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||
|
@ -3,8 +3,6 @@
|
||||
<PropertyGroup>
|
||||
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1574;CS9113;CS1998</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
@ -77,4 +75,8 @@
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Infrastructure.IntegrationTest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -128,6 +128,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
|
||||
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()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TwoFactorProviders))
|
||||
@ -137,19 +141,17 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
|
||||
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
|
||||
if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f))
|
||||
{
|
||||
_twoFactorProviders.Remove(TwoFactorProviderType.U2f);
|
||||
}
|
||||
/*
|
||||
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.
|
||||
This will probably exist in perpetuity since there is no way to know for sure if any
|
||||
given user does or doesn't have this enabled. It is a non-zero chance.
|
||||
*/
|
||||
_twoFactorProviders?.Remove(TwoFactorProviderType.U2f);
|
||||
|
||||
return _twoFactorProviders;
|
||||
}
|
||||
@ -169,6 +171,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
public void ClearTwoFactorProviders()
|
||||
{
|
||||
SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor
|
||||
/// 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)
|
||||
{
|
||||
var providers = GetTwoFactorProviders();
|
||||
if (providers == null || !providers.ContainsKey(provider))
|
||||
if (providers == null || !providers.TryGetValue(provider, out var value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return providers[provider];
|
||||
return value;
|
||||
}
|
||||
|
||||
public long StorageBytesRemaining()
|
||||
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -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}}
|
@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel
|
||||
public bool ToDelete { get; set; }
|
||||
|
||||
public bool CloudSponsorshipRemoved { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
|
||||
public OrganizationSponsorshipResponseModel() { }
|
||||
|
||||
@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel
|
||||
ValidUntil = sponsorshipData.ValidUntil;
|
||||
ToDelete = sponsorshipData.ToDelete;
|
||||
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
|
||||
IsAdminInitiated = sponsorshipData.IsAdminInitiated;
|
||||
}
|
||||
|
||||
public OrganizationSponsorshipData ToOrganizationSponsorship()
|
||||
@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel
|
||||
LastSyncDate = LastSyncDate,
|
||||
ValidUntil = ValidUntil,
|
||||
ToDelete = ToDelete,
|
||||
CloudSponsorshipRemoved = CloudSponsorshipRemoved
|
||||
CloudSponsorshipRemoved = CloudSponsorshipRemoved,
|
||||
IsAdminInitiated = IsAdminInitiated,
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,16 @@ public interface IUserRepository : IRepository<User, Guid>
|
||||
/// </summary>
|
||||
Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);
|
||||
/// <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.
|
||||
/// <para>Warning: Any user key encrypted data not included will be lost.</para>
|
||||
/// </summary>
|
||||
|
@ -40,7 +40,6 @@ public interface IMailService
|
||||
Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable<string> adminEmails, 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 SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, 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 SendNewDeviceLoggedInEmail(string email, string deviceType, 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 SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email);
|
||||
Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email);
|
||||
@ -88,9 +86,6 @@ public interface IMailService
|
||||
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
||||
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 SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
|
@ -71,11 +71,13 @@ public interface IUserService
|
||||
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
|
||||
int? version = null);
|
||||
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> 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<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.
|
||||
/// The organization must be enabled and able to have verified domains.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// False if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
Task<bool> IsClaimedByAnyOrganizationAsync(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Verify whether the new email domain meets the requirements for managed users.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// IdentityResult
|
||||
/// </returns>
|
||||
@ -149,9 +146,6 @@ public interface IUserService
|
||||
/// <summary>
|
||||
/// Gets the organizations that manage the user.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An empty collection if the Account Deprovisioning feature flag is disabled.
|
||||
/// </returns>
|
||||
/// <inheritdoc cref="IsClaimedByAnyOrganizationAsync"/>
|
||||
Task<IEnumerable<Organization>> GetOrganizationsClaimingUserAsync(Guid userId);
|
||||
}
|
||||
|
@ -301,20 +301,6 @@ public class HandlebarsMailService : IMailService
|
||||
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)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
@ -532,20 +518,6 @@ public class HandlebarsMailService : IMailService
|
||||
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)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email);
|
||||
@ -1137,53 +1109,6 @@ public class HandlebarsMailService : IMailService
|
||||
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)
|
||||
{
|
||||
var message = CreateDefaultMessage("Domain not claimed", adminEmails);
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
@ -77,6 +78,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
|
||||
public UserService(
|
||||
@ -115,6 +117,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
IPremiumUserBillingService premiumUserBillingService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IDistributedCache distributedCache)
|
||||
: base(
|
||||
store,
|
||||
@ -158,6 +161,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
_premiumUserBillingService = premiumUserBillingService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_distributedCache = distributedCache;
|
||||
}
|
||||
|
||||
@ -918,7 +922,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
await SaveUserAsync(user);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
||||
|
||||
if (!await TwoFactorIsEnabledAsync(user))
|
||||
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||
{
|
||||
await CheckPoliciesOnTwoFactorRemovalAsync(user);
|
||||
}
|
||||
@ -1280,48 +1284,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
orgAbility.UsersGetPremium &&
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return Enumerable.Empty<Organization>();
|
||||
}
|
||||
|
||||
// Get all organizations that have verified the user's email domain.
|
||||
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
|
||||
|
||||
@ -1443,22 +1400,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
p.OrganizationId,
|
||||
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
|
||||
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);
|
||||
}
|
||||
|
||||
await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync(
|
||||
new RevokeOrganizationUsersRequest(
|
||||
p.OrganizationId,
|
||||
[new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }],
|
||||
new SystemUser(EventSystemUser.TwoFactorDisabled)));
|
||||
await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email);
|
||||
}).ToArray();
|
||||
|
||||
await Task.WhenAll(removeOrgUserTasks);
|
||||
|
@ -80,11 +80,6 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) =>
|
||||
Task.CompletedTask;
|
||||
|
||||
@ -155,11 +150,6 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
@ -268,21 +258,6 @@ public class NoopMailService : IMailService
|
||||
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)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual bool EnableCloudCommunication { get; set; } = false;
|
||||
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
|
||||
public virtual string EventGridKey { get; set; }
|
||||
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
|
||||
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
|
||||
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||
public virtual string DatabaseProvider { get; set; }
|
||||
@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
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 string ApiKey { get; set; }
|
||||
|
@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
|
||||
cipher.Value.ViewPassword = true;
|
||||
}
|
||||
}
|
||||
else if (await CanAccessUnassignedCiphersAsync(org))
|
||||
else if (CanAccessUnassignedCiphers(org))
|
||||
{
|
||||
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
|
||||
foreach (var unassignedCipher in unassignedCiphers)
|
||||
@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> CanAccessUnassignedCiphersAsync(CurrentContextOrganization org)
|
||||
private bool CanAccessUnassignedCiphers(CurrentContextOrganization org)
|
||||
{
|
||||
if (org is
|
||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
|
||||
|
||||
namespace Bit.Core.Vault.Repositories;
|
||||
|
@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.Registration;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
using Bit.Core.Context;
|
||||
@ -37,7 +36,6 @@ public class AccountsController : Controller
|
||||
private readonly ILogger<AccountsController> _logger;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IRegisterUserCommand _registerUserCommand;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||
@ -85,7 +83,6 @@ public class AccountsController : Controller
|
||||
ILogger<AccountsController> logger,
|
||||
IUserRepository userRepository,
|
||||
IRegisterUserCommand registerUserCommand,
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
||||
@ -99,7 +96,6 @@ public class AccountsController : Controller
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_registerUserCommand = registerUserCommand;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
||||
@ -167,7 +163,7 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("register/finish")]
|
||||
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
||||
public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
||||
return new RegisterResponseModel(captchaBypassToken);
|
||||
return new RegisterFinishResponseModel();
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
@ -9,7 +8,7 @@ public class CustomValidatorRequestContext
|
||||
public User User { get; set; }
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Device Device { get; set; }
|
||||
@ -39,5 +38,4 @@ public class CustomValidatorRequestContext
|
||||
/// This will be null if the authentication request is successful.
|
||||
/// </summary>
|
||||
public Dictionary<string, object> CustomResponse { get; set; }
|
||||
public CaptchaResponse CaptchaResponse { get; set; }
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
@ -49,7 +48,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
@ -66,7 +64,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_deviceValidator = deviceValidator;
|
||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
CurrentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
@ -81,23 +78,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
// 1. We need to check if the user is a bot and if their master password hash is correct.
|
||||
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
|
||||
// 1. We need to check if the user's master password hash is correct.
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (!valid || isBot)
|
||||
if (!valid)
|
||||
{
|
||||
if (isBot)
|
||||
{
|
||||
_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 UpdateFailedAuthDetailsAsync(user);
|
||||
|
||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||
return;
|
||||
@ -167,7 +153,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||
}
|
||||
return;
|
||||
@ -379,7 +365,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
}
|
||||
|
||||
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
|
||||
private async Task UpdateFailedAuthDetailsAsync(User user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
@ -390,32 +376,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
user.FailedLoginCount = ++user.FailedLoginCount;
|
||||
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
||||
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)
|
||||
|
@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
|
@ -22,8 +22,7 @@ public class DeviceValidator(
|
||||
ICurrentContext currentContext,
|
||||
IUserService userService,
|
||||
IDistributedCache distributedCache,
|
||||
ILogger<DeviceValidator> logger,
|
||||
IFeatureService featureService) : IDeviceValidator
|
||||
ILogger<DeviceValidator> logger) : IDeviceValidator
|
||||
{
|
||||
private readonly IDeviceService _deviceService = deviceService;
|
||||
private readonly IDeviceRepository _deviceRepository = deviceRepository;
|
||||
@ -33,7 +32,6 @@ public class DeviceValidator(
|
||||
private readonly IUserService _userService = userService;
|
||||
private readonly IDistributedCache distributedCache = distributedCache;
|
||||
private readonly ILogger<DeviceValidator> _logger = logger;
|
||||
private readonly IFeatureService _featureService = featureService;
|
||||
|
||||
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
|
||||
// PM-13340: remove feature flag
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) &&
|
||||
request.GrantType == "password" &&
|
||||
if (request.GrantType == "password" &&
|
||||
request.Raw["AuthRequest"] == null &&
|
||||
!context.TwoFactorRequired &&
|
||||
!context.SsoRequired &&
|
||||
|
@ -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);
|
||||
}
|
@ -3,7 +3,6 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
@ -21,7 +20,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
public ResourceOwnerPasswordValidator(
|
||||
@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
ICaptchaValidationService captchaValidationService,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
@ -63,7 +58,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_deviceValidator = deviceValidator;
|
||||
}
|
||||
@ -88,37 +82,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
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);
|
||||
if (context.Result.CustomResponse != null && bypassToken != null)
|
||||
{
|
||||
context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken;
|
||||
}
|
||||
}
|
||||
|
||||
protected async override Task<bool> ValidateContextAsync(ResourceOwnerPasswordValidationContext context,
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@ -16,56 +17,25 @@ using Microsoft.AspNetCore.Identity;
|
||||
|
||||
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(
|
||||
IUserService userService,
|
||||
UserManager<User> userManager,
|
||||
IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
ICurrentContext currentContext) : ITwoFactorAuthenticationValidator
|
||||
{
|
||||
private readonly IUserService _userService = userService;
|
||||
private readonly UserManager<User> _userManager = userManager;
|
||||
private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider;
|
||||
private readonly IFeatureService _featureService = featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository = organizationRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
|
||||
public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
@ -121,7 +91,10 @@ public class TwoFactorAuthenticationValidator(
|
||||
{ "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))
|
||||
{
|
||||
twoFactorResultDict.Add("SsoEmail2faSessionToken",
|
||||
@ -130,12 +103,6 @@ public class TwoFactorAuthenticationValidator(
|
||||
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;
|
||||
}
|
||||
|
||||
@ -161,7 +128,7 @@ public class TwoFactorAuthenticationValidator(
|
||||
|
||||
// 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
|
||||
// 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
|
||||
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
|
||||
|
||||
// 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 because it is a "special" 2FA type that isn't ever explicitly
|
||||
// is enabled and allowed by their premium status.
|
||||
// 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. We just have to check if the token is valid.
|
||||
if (type != TwoFactorProviderType.Remember &&
|
||||
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
|
||||
user.GetTwoFactorProvider(type) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -9,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Identity.Models.Request.Accounts;
|
||||
|
||||
public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
|
||||
public class RegisterRequestModel : IValidatableObject
|
||||
{
|
||||
[StringLength(50)]
|
||||
public string Name { get; set; }
|
||||
@ -22,7 +21,6 @@ public class RegisterRequestModel : IValidatableObject, ICaptchaProtectedModel
|
||||
public string MasterPasswordHash { get; set; }
|
||||
[StringLength(50)]
|
||||
public string MasterPasswordHint { get; set; }
|
||||
public string CaptchaResponse { get; set; }
|
||||
public string Key { get; set; }
|
||||
public KeysRequestModel Keys { get; set; }
|
||||
public string Token { get; set; }
|
||||
|
@ -1,5 +0,0 @@
|
||||
namespace Bit.Identity.Models.Response.Accounts;
|
||||
public interface ICaptchaProtectedResponseModel
|
||||
{
|
||||
public string CaptchaBypassToken { get; set; }
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Identity.Models.Response.Accounts;
|
||||
|
||||
public class RegisterFinishResponseModel : ResponseModel
|
||||
{
|
||||
public RegisterFinishResponseModel()
|
||||
: base("registerFinish")
|
||||
{ }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -17,9 +17,6 @@
|
||||
},
|
||||
"braintree": {
|
||||
"production": true
|
||||
},
|
||||
"captcha": {
|
||||
"maximumFailedLoginAttempts": 5
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
|
@ -14,9 +14,6 @@
|
||||
"internalVault": null,
|
||||
"internalSso": null,
|
||||
"internalScim": null
|
||||
},
|
||||
"captcha": {
|
||||
"maximumFailedLoginAttempts": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8618</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
@ -253,7 +253,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task UpdateUserKeyAndEncryptedDataV2Async(
|
||||
User user,
|
||||
IEnumerable<UpdateEncryptedDataForKeyRotation> updateDataActions)
|
||||
@ -289,7 +288,6 @@ public class UserRepository : Repository<User, Guid>, IUserRepository
|
||||
UnprotectData(user);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<User>> GetManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (user == null)
|
||||
|
@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
public IQueryable<OrganizationUserOrganizationDetails> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization
|
||||
from o in outerOrganization.DefaultIfEmpty()
|
||||
join o in dbContext.Organizations on ou.OrganizationId equals o.Id
|
||||
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()
|
||||
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,
|
||||
LimitCollectionCreation = o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = o.LimitCollectionDeletion,
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
||||
UseRiskInsights = o.UseRiskInsights,
|
||||
UseAdminSponsoredFamilies = o.UseAdminSponsoredFamilies,
|
||||
LimitItemDeletion = o.LimitItemDeletion,
|
||||
IsAdminInitiated = os.IsAdminInitiated
|
||||
};
|
||||
return query;
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0108;CS8632</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="linq2db" Version="5.4.1" />
|
||||
|
@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Platform;
|
||||
|
||||
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 Installation : C.Installation;
|
||||
|
||||
public class InstallationMapperProfile : Profile
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using DataModel = Bit.Core.Models.Data;
|
||||
|
||||
#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())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
return await GetDbSet(dbContext).Where(e => e.Email == email)
|
||||
.Select(e => new DataModel.UserKdfInformation
|
||||
.Select(e => new UserKdfInformation
|
||||
{
|
||||
Kdf = e.Kdf,
|
||||
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())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
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
|
||||
.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)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
@ -151,14 +151,6 @@ public static class ServiceCollectionExtensions
|
||||
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 =>
|
||||
new DataProtectorTokenFactory<SsoTokenable>(
|
||||
SsoTokenable.ClearTextPrefix,
|
||||
@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build">
|
||||
<Sdk Name="Microsoft.Build.Sql" Version="0.1.9-preview" />
|
||||
<Sdk Name="Microsoft.Build.Sql"/>
|
||||
<PropertyGroup>
|
||||
<Name>Sql</Name>
|
||||
<ProjectGuid>{58554e52-fdec-4832-aff9-302b01e08dca}</ProjectGuid>
|
||||
|
@ -3,9 +3,9 @@ CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
|
||||
DECLARE @Domain NVARCHAR(256)
|
||||
|
||||
|
||||
SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))
|
||||
|
||||
SELECT
|
||||
@ -19,8 +19,8 @@ BEGIN
|
||||
[dbo].[OrganizationView] O
|
||||
INNER JOIN [dbo].[OrganizationDomainView] OD
|
||||
ON O.Id = OD.OrganizationId
|
||||
LEFT JOIN [dbo].[Ssoconfig] S
|
||||
LEFT JOIN [dbo].[SsoConfig] S
|
||||
ON O.Id = S.OrganizationId
|
||||
WHERE OD.DomainName = @Domain
|
||||
AND O.Enabled = 1
|
||||
END
|
||||
END
|
||||
|
@ -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
|
@ -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
|
@ -15,7 +15,7 @@ BEGIN
|
||||
OD.DomainName
|
||||
FROM [dbo].[OrganizationView] O
|
||||
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
|
||||
AND O.Enabled = 1
|
||||
AND OD.VerifiedDate IS NOT NULL
|
||||
|
@ -51,7 +51,8 @@ SELECT
|
||||
O.[AllowAdminAccessToAllCollectionItems],
|
||||
O.[UseRiskInsights],
|
||||
O.[UseAdminSponsoredFamilies],
|
||||
O.[LimitItemDeletion]
|
||||
O.[LimitItemDeletion],
|
||||
OS.[IsAdminInitiated]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Api\Api.csproj" />
|
||||
<ProjectReference Include="..\..\util\Seeder\Seeder.csproj" />
|
||||
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
|
||||
|
||||
<Content Include="..\..\src\Api\appsettings.*.json">
|
||||
|
@ -238,20 +238,13 @@ public class OrganizationUsersControllerTests
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
[Theory, BitAutoData]
|
||||
public async Task Get_ReturnsUser(
|
||||
bool accountDeprovisioningEnabled,
|
||||
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.Permissions = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(accountDeprovisioningEnabled);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManageUsers(organizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
@ -267,8 +260,8 @@ public class OrganizationUsersControllerTests
|
||||
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
|
||||
|
||||
Assert.Equal(organizationUser.Id, response.Id);
|
||||
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization);
|
||||
Assert.Equal(accountDeprovisioningEnabled, response.ClaimedByOrganization);
|
||||
Assert.True(response.ManagedByOrganization);
|
||||
Assert.True(response.ClaimedByOrganization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -140,7 +140,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { null });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
@ -170,7 +169,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization> { { foundOrg } });
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
|
||||
|
||||
@ -205,7 +203,6 @@ public class OrganizationsControllerTests : IDisposable
|
||||
_currentContext.OrganizationUser(orgId).Returns(true);
|
||||
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List<Organization>());
|
||||
|
||||
await _sut.Leave(orgId);
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8620;CS0169</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -7,13 +7,13 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -40,6 +40,7 @@ public class AccountsControllerTests : IDisposable
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
@ -64,6 +65,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_policyService = Substitute.For<IPolicyService>();
|
||||
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
|
||||
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
|
||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_cipherValidator =
|
||||
@ -87,6 +89,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_setInitialMasterPasswordCommand,
|
||||
_tdeOffboardingPasswordCommand,
|
||||
_rotateUserKeyCommand,
|
||||
_twoFactorIsEnabledQuery,
|
||||
_featureService,
|
||||
_cipherValidator,
|
||||
_folderValidator,
|
||||
@ -189,21 +192,6 @@ public class AccountsControllerTests : IDisposable
|
||||
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]
|
||||
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
|
||||
{
|
||||
@ -533,12 +521,11 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||
public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
|
||||
@ -547,12 +534,11 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||
public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
|
||||
|
||||
|
@ -216,6 +216,12 @@ public class OrganizationSponsorshipsControllerTests
|
||||
sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
|
||||
.GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships);
|
||||
|
||||
// Set IsAdminInitiated to true for all test sponsorships
|
||||
foreach (var sponsorship in sponsorships)
|
||||
{
|
||||
sponsorship.IsAdminInitiated = true;
|
||||
}
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user