mirror of
https://github.com/bitwarden/server.git
synced 2025-06-29 15:16:15 -05:00
Merge branch 'main' into ac/pm-19145-refactor-OrganizationService.ImportAsync
This commit is contained in:
commit
5b09101505
66
.github/renovate.json5
vendored
66
.github/renovate.json5
vendored
@ -20,7 +20,7 @@
|
|||||||
],
|
],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
reviewers: ["team:dept-bre"],
|
reviewers: ["team:dept-bre"],
|
||||||
addLabels: ["hold"]
|
addLabels: ["hold"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
groupName: "dockerfile minor",
|
groupName: "dockerfile minor",
|
||||||
@ -37,6 +37,16 @@
|
|||||||
matchManagers: ["github-actions"],
|
matchManagers: ["github-actions"],
|
||||||
matchUpdateTypes: ["minor"],
|
matchUpdateTypes: ["minor"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates.
|
||||||
|
// This overrides the default that ignores patch updates for nuget dependencies.
|
||||||
|
matchPackageNames: [
|
||||||
|
"/^Microsoft\\.Extensions\\./",
|
||||||
|
"/^Microsoft\\.AspNetCore\\./",
|
||||||
|
],
|
||||||
|
matchUpdateTypes: ["patch"],
|
||||||
|
dependencyDashboardApproval: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
matchManagers: ["dockerfile", "docker-compose"],
|
matchManagers: ["dockerfile", "docker-compose"],
|
||||||
commitMessagePrefix: "[deps] BRE:",
|
commitMessagePrefix: "[deps] BRE:",
|
||||||
@ -59,6 +69,7 @@
|
|||||||
"DuoUniversal",
|
"DuoUniversal",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
|
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
||||||
"Microsoft.Extensions.Identity.Stores",
|
"Microsoft.Extensions.Identity.Stores",
|
||||||
"Otp.NET",
|
"Otp.NET",
|
||||||
"Sustainsys.Saml2.AspNetCore2",
|
"Sustainsys.Saml2.AspNetCore2",
|
||||||
@ -79,8 +90,6 @@
|
|||||||
"CsvHelper",
|
"CsvHelper",
|
||||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||||
"Microsoft.AspNetCore.Mvc.Testing",
|
"Microsoft.AspNetCore.Mvc.Testing",
|
||||||
"Microsoft.Extensions.Logging",
|
|
||||||
"Microsoft.Extensions.Logging.Console",
|
|
||||||
"Newtonsoft.Json",
|
"Newtonsoft.Json",
|
||||||
"NSubstitute",
|
"NSubstitute",
|
||||||
"Sentry.Serilog",
|
"Sentry.Serilog",
|
||||||
@ -100,9 +109,9 @@
|
|||||||
reviewers: ["team:team-billing-dev"],
|
reviewers: ["team:team-billing-dev"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Logging"],
|
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||||
groupName: "Microsoft.Extensions.Logging",
|
groupName: "EntityFrameworkCore",
|
||||||
description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset",
|
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
@ -117,9 +126,6 @@
|
|||||||
"Microsoft.EntityFrameworkCore.Relational",
|
"Microsoft.EntityFrameworkCore.Relational",
|
||||||
"Microsoft.EntityFrameworkCore.Sqlite",
|
"Microsoft.EntityFrameworkCore.Sqlite",
|
||||||
"Microsoft.EntityFrameworkCore.SqlServer",
|
"Microsoft.EntityFrameworkCore.SqlServer",
|
||||||
"Microsoft.Extensions.Caching.Cosmos",
|
|
||||||
"Microsoft.Extensions.Caching.SqlServer",
|
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
|
||||||
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
"Npgsql.EntityFrameworkCore.PostgreSQL",
|
||||||
"Pomelo.EntityFrameworkCore.MySql",
|
"Pomelo.EntityFrameworkCore.MySql",
|
||||||
],
|
],
|
||||||
@ -142,56 +148,40 @@
|
|||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
"Azure.Storage.Queues",
|
"Azure.Storage.Queues",
|
||||||
"Microsoft.AspNetCore.Authentication.JwtBearer",
|
"LaunchDarkly.ServerSdk",
|
||||||
"Microsoft.AspNetCore.Http",
|
"Microsoft.AspNetCore.Http",
|
||||||
|
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
||||||
|
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
||||||
|
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
||||||
|
"Microsoft.Extensions.Configuration.UserSecrets",
|
||||||
|
"Microsoft.Extensions.Configuration",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
||||||
|
"Microsoft.Extensions.DependencyInjection",
|
||||||
|
"Microsoft.Extensions.Logging",
|
||||||
|
"Microsoft.Extensions.Logging.Console",
|
||||||
|
"Microsoft.Extensions.Caching.Cosmos",
|
||||||
|
"Microsoft.Extensions.Caching.SqlServer",
|
||||||
|
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||||
"Quartz",
|
"Quartz",
|
||||||
],
|
],
|
||||||
description: "Platform owned dependencies",
|
description: "Platform owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Platform:",
|
commitMessagePrefix: "[deps] Platform:",
|
||||||
reviewers: ["team:team-platform-dev"],
|
reviewers: ["team:team-platform-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"],
|
|
||||||
groupName: "EntityFrameworkCore",
|
|
||||||
description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
"AutoMapper.Extensions.Microsoft.DependencyInjection",
|
||||||
"AWSSDK.SimpleEmail",
|
"AWSSDK.SimpleEmail",
|
||||||
"AWSSDK.SQS",
|
"AWSSDK.SQS",
|
||||||
"Handlebars.Net",
|
"Handlebars.Net",
|
||||||
"LaunchDarkly.ServerSdk",
|
|
||||||
"MailKit",
|
"MailKit",
|
||||||
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack",
|
|
||||||
"Microsoft.AspNetCore.SignalR.StackExchangeRedis",
|
|
||||||
"Microsoft.Azure.NotificationHubs",
|
"Microsoft.Azure.NotificationHubs",
|
||||||
"Microsoft.Extensions.Configuration.EnvironmentVariables",
|
|
||||||
"Microsoft.Extensions.Configuration.UserSecrets",
|
|
||||||
"Microsoft.Extensions.Configuration",
|
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions",
|
|
||||||
"Microsoft.Extensions.DependencyInjection",
|
|
||||||
"SendGrid",
|
"SendGrid",
|
||||||
],
|
],
|
||||||
description: "Tools owned dependencies",
|
description: "Tools owned dependencies",
|
||||||
commitMessagePrefix: "[deps] Tools:",
|
commitMessagePrefix: "[deps] Tools:",
|
||||||
reviewers: ["team:team-tools-dev"],
|
reviewers: ["team:team-tools-dev"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"],
|
|
||||||
groupName: "SignalR",
|
|
||||||
description: "Group SignalR to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.Configuration"],
|
|
||||||
groupName: "Microsoft.Extensions.Configuration",
|
|
||||||
description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"],
|
|
||||||
groupName: "Microsoft.Extensions.DependencyInjection",
|
|
||||||
description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"AngleSharp",
|
"AngleSharp",
|
||||||
|
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@ -14,6 +14,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
_GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -234,12 +235,18 @@ jobs:
|
|||||||
- name: Generate Docker image tag
|
- name: Generate Docker image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
||||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only
|
||||||
else
|
else
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only
|
||||||
|
IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag
|
||||||
|
IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$IMAGE_TAG" == "main" ]]; then
|
if [[ "$IMAGE_TAG" == "main" ]]; then
|
||||||
IMAGE_TAG=dev
|
IMAGE_TAG=dev
|
||||||
fi
|
fi
|
||||||
|
@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -325,6 +329,14 @@ Global
|
|||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -377,6 +389,8 @@ Global
|
|||||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
{3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||||
|
{9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
|
{17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||||
|
@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -76,9 +76,8 @@ public class PostUserCommand(
|
|||||||
var invitedOrganizationUserId = result switch
|
var invitedOrganizationUserId = result switch
|
||||||
{
|
{
|
||||||
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
Success<ScimInviteOrganizationUsersResponse> success => success.Value.InvitedUser.Id,
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors
|
Failure<ScimInviteOrganizationUsersResponse> { Error.Message: NoUsersToInviteError.Code } => (Guid?)null,
|
||||||
.Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null,
|
Failure<ScimInviteOrganizationUsersResponse> failure => throw MapToBitException(failure.Error),
|
||||||
Failure<ScimInviteOrganizationUsersResponse> failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors),
|
|
||||||
_ => throw new InvalidOperationException()
|
_ => throw new InvalidOperationException()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"rollForward": "latestFeature"
|
"rollForward": "latestFeature"
|
||||||
},
|
},
|
||||||
"msbuild-sdks": {
|
"msbuild-sdks": {
|
||||||
"Microsoft.Build.Traversal": "4.1.0"
|
"Microsoft.Build.Traversal": "4.1.0",
|
||||||
|
"Microsoft.Build.Sql": "0.1.9-preview"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,7 +167,6 @@ public class UsersController : Controller
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||||
{
|
{
|
||||||
var user = await _userRepository.GetByIdAsync(id);
|
var user = await _userRepository.GetByIdAsync(id);
|
||||||
|
@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
ProviderName = organization.ProviderName;
|
ProviderName = organization.ProviderName;
|
||||||
ProviderType = organization.ProviderType;
|
ProviderType = organization.ProviderType;
|
||||||
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName;
|
||||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
IsAdminInitiated = organization.IsAdminInitiated ?? false;
|
||||||
|
FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) &&
|
||||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||||
.UsersCanSponsor(organization);
|
.UsersCanSponsor(organization);
|
||||||
ProductTierType = organization.PlanType.GetProductTier();
|
ProductTierType = organization.PlanType.GetProductTier();
|
||||||
@ -157,4 +158,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool UserIsClaimedByOrganization { get; set; }
|
public bool UserIsClaimedByOrganization { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
return new NotFoundResult();
|
return new NotFoundResult();
|
||||||
}
|
}
|
||||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser),
|
||||||
collections);
|
collections);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
@ -185,7 +185,7 @@ public class MembersController : Controller
|
|||||||
{
|
{
|
||||||
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id);
|
||||||
response = new MemberResponseModel(existingUserDetails,
|
response = new MemberResponseModel(existingUserDetails,
|
||||||
await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities;
|
|||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -45,6 +46,7 @@ public class AccountsController : Controller
|
|||||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
|
||||||
@ -68,6 +70,7 @@ public class AccountsController : Controller
|
|||||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
|
||||||
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
|
||||||
@ -87,6 +90,7 @@ public class AccountsController : Controller
|
|||||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_cipherValidator = cipherValidator;
|
_cipherValidator = cipherValidator;
|
||||||
_folderValidator = folderValidator;
|
_folderValidator = folderValidator;
|
||||||
@ -389,7 +393,7 @@ public class AccountsController : Controller
|
|||||||
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
|
||||||
ProviderUserStatusType.Confirmed);
|
ProviderUserStatusType.Confirmed);
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -423,7 +427,7 @@ public class AccountsController : Controller
|
|||||||
|
|
||||||
await _userService.SaveUserAsync(model.ToUser(user));
|
await _userService.SaveUserAsync(model.ToUser(user));
|
||||||
|
|
||||||
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -442,7 +446,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
await _userService.SaveUserAsync(model.ToUser(user), true);
|
await _userService.SaveUserAsync(model.ToUser(user), true);
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
@ -693,7 +697,6 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpPost("resend-new-device-otp")]
|
[HttpPost("resend-new-device-otp")]
|
||||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.Models.Request;
|
|||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers;
|
|||||||
[Route("accounts")]
|
[Route("accounts")]
|
||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
IUserService userService) : Controller
|
IUserService userService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@ -56,7 +58,7 @@ public class AccountsController(
|
|||||||
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license,
|
||||||
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode });
|
||||||
|
|
||||||
var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using System.Diagnostics;
|
||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
@ -292,6 +293,7 @@ public class OrganizationBillingController(
|
|||||||
sale.SubscriptionSetup.SkipTrial = true;
|
sale.SubscriptionSetup.SkipTrial = true;
|
||||||
await organizationBillingService.Finalize(sale);
|
await organizationBillingService.Finalize(sale);
|
||||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
var org = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
|
||||||
if (organizationSignup.PaymentMethodType != null)
|
if (organizationSignup.PaymentMethodType != null)
|
||||||
{
|
{
|
||||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||||
|
@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId);
|
||||||
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(sponsorships.Select(s =>
|
return new ListResponseModel<OrganizationSponsorshipInvitesResponseModel>(
|
||||||
new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))));
|
sponsorships
|
||||||
|
.Where(s => s.IsAdminInitiated)
|
||||||
|
.Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Api.Utilities;
|
|
||||||
|
|
||||||
public static class CommandResultExtensions
|
|
||||||
{
|
|
||||||
public static IActionResult MapToActionResult<T>(this CommandResult<T> commandResult)
|
|
||||||
{
|
|
||||||
return commandResult switch
|
|
||||||
{
|
|
||||||
NoRecordFoundFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
|
||||||
BadRequestFailure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Failure<T> failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Success<T> success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK },
|
|
||||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IActionResult MapToActionResult(this CommandResult commandResult)
|
|
||||||
{
|
|
||||||
return commandResult switch
|
|
||||||
{
|
|
||||||
NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound },
|
|
||||||
BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest },
|
|
||||||
Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK },
|
|
||||||
_ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -37,6 +38,7 @@ public class SyncController : Controller
|
|||||||
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
|
||||||
public SyncController(
|
public SyncController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@ -51,7 +53,8 @@ public class SyncController : Controller
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IApplicationCacheService applicationCacheService)
|
IApplicationCacheService applicationCacheService,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -66,6 +69,7 @@ public class SyncController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -102,7 +106,7 @@ public class SyncController : Controller
|
|||||||
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||||
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id);
|
||||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||||
|
@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
|
public bool? IsAdminInitiated { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
|
|
||||||
public AcceptOrgUserCommand(
|
public AcceptOrgUserCommand(
|
||||||
@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory)
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
// Enforce Two Factor Authentication Policy of organization user is trying to join
|
||||||
if (!await userService.TwoFactorIsEnabledAsync(user))
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id,
|
||||||
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
using Bit.Core.Models.Commands;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Models.Commands;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Errors;
|
|
||||||
using Bit.Core.AdminConsole.Interfaces;
|
using Bit.Core.AdminConsole.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -49,11 +49,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
|||||||
{
|
{
|
||||||
case Failure<InviteOrganizationUsersResponse> failure:
|
case Failure<InviteOrganizationUsersResponse> failure:
|
||||||
return new Failure<ScimInviteOrganizationUsersResponse>(
|
return new Failure<ScimInviteOrganizationUsersResponse>(
|
||||||
failure.Errors.Select(error => new Error<ScimInviteOrganizationUsersResponse>(error.Message,
|
new Error<ScimInviteOrganizationUsersResponse>(failure.Error.Message,
|
||||||
new ScimInviteOrganizationUsersResponse
|
new ScimInviteOrganizationUsersResponse
|
||||||
{
|
{
|
||||||
InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault()
|
InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault()
|
||||||
})));
|
}));
|
||||||
|
|
||||||
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
case Success<InviteOrganizationUsersResponse> success when success.Value.InvitedUsers.Any():
|
||||||
var user = success.Value.InvitedUsers.First();
|
var user = success.Value.InvitedUsers.First();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Business;
|
using Bit.Core.AdminConsole.Models.Business;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Models.Data;
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Commands;
|
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
using Bit.Core.AdminConsole.Errors;
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
|
||||||
|
|
||||||
public abstract record ValidationResult<T>;
|
|
||||||
|
|
||||||
public record Valid<T> : ValidationResult<T>
|
|
||||||
{
|
|
||||||
public Valid() { }
|
|
||||||
|
|
||||||
public Valid(T Value)
|
|
||||||
{
|
|
||||||
this.Value = Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T Value { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Invalid<T> : ValidationResult<T>
|
|
||||||
{
|
|
||||||
public IEnumerable<Error<T>> Errors { get; init; } = [];
|
|
||||||
|
|
||||||
public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message));
|
|
||||||
|
|
||||||
public Invalid() { }
|
|
||||||
|
|
||||||
public Invalid(Error<T> error) : this([error]) { }
|
|
||||||
|
|
||||||
public Invalid(IEnumerable<Error<T>> errors)
|
|
||||||
{
|
|
||||||
Errors = errors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ValidationResultMappers
|
|
||||||
{
|
|
||||||
public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>
|
|
||||||
validationResult switch
|
|
||||||
{
|
|
||||||
Valid<A> => new Valid<B>(invalidValue),
|
|
||||||
Invalid<A> invalid => new Invalid<B>(invalid.Errors.Select(x => x.ToError(invalidValue))),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type")
|
|
||||||
};
|
|
||||||
}
|
|
51
src/Core/AdminConsole/Utilities/Commands/CommandResult.cs
Normal file
51
src/Core/AdminConsole/Utilities/Commands/CommandResult.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
using Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Utilities.Commands;
|
||||||
|
|
||||||
|
public abstract class CommandResult<T>;
|
||||||
|
|
||||||
|
public class Success<T>(T value) : CommandResult<T>
|
||||||
|
{
|
||||||
|
public T Value { get; } = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Failure<T>(Error<T> error) : CommandResult<T>
|
||||||
|
{
|
||||||
|
public Error<T> Error { get; } = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Partial<T>(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
|
||||||
|
: CommandResult<T>
|
||||||
|
{
|
||||||
|
public IEnumerable<T> Successes { get; } = successfulItems;
|
||||||
|
public IEnumerable<Error<T>> Failures { get; } = failedItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CommandResultExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="invalidResult">This is the invalid type from validating the object.</param>
|
||||||
|
/// <param name="mappingFunction">This function will map between the two types for the inner ErrorT</param>
|
||||||
|
/// <typeparam name="A">Invalid object's type</typeparam>
|
||||||
|
/// <typeparam name="B">Failure object's type</typeparam>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>
|
||||||
|
new Failure<B>(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("Use CommandResult<T> instead. This will be removed once old code is updated.")]
|
||||||
|
public class CommandResult(IEnumerable<string> errors)
|
||||||
|
{
|
||||||
|
public CommandResult(string error) : this([error]) { }
|
||||||
|
|
||||||
|
public bool Success => ErrorMessages.Count == 0;
|
||||||
|
public bool HasErrors => ErrorMessages.Count > 0;
|
||||||
|
public List<string> ErrorMessages { get; } = errors.ToList();
|
||||||
|
public CommandResult() : this(Array.Empty<string>()) { }
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.AdminConsole.Errors;
|
namespace Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
public record Error<T>(string Message, T ErroredValue);
|
public record Error<T>(string Message, T ErroredValue);
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.AdminConsole.Errors;
|
namespace Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
public record InsufficientPermissionsError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.AdminConsole.Errors;
|
namespace Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
public record InvalidResultTypeError<T>(T Value) : Error<T>(Code, Value)
|
public record InvalidResultTypeError<T>(T Value) : Error<T>(Code, Value)
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.AdminConsole.Errors;
|
namespace Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
public record RecordNotFoundError<T>(string Message, T ErroredValue) : Error<T>(Message, ErroredValue)
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
namespace Bit.Core.AdminConsole.Shared.Validation;
|
namespace Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
public interface IValidator<T>
|
public interface IValidator<T>
|
||||||
{
|
{
|
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.AdminConsole.Utilities.Errors;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Utilities.Validation;
|
||||||
|
|
||||||
|
public abstract record ValidationResult<T>;
|
||||||
|
|
||||||
|
public record Valid<T>(T Value) : ValidationResult<T>;
|
||||||
|
|
||||||
|
public record Invalid<T>(Error<T> Error) : ValidationResult<T>;
|
||||||
|
|
||||||
|
public static class ValidationResultMappers
|
||||||
|
{
|
||||||
|
public static ValidationResult<B> Map<A, B>(this ValidationResult<A> validationResult, B invalidValue) =>
|
||||||
|
validationResult switch
|
||||||
|
{
|
||||||
|
Valid<A> => new Valid<B>(invalidValue),
|
||||||
|
Invalid<A> invalid => new Invalid<B>(invalid.Error.ToError(invalidValue)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type")
|
||||||
|
};
|
||||||
|
}
|
@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte
|
|||||||
Email = 1,
|
Email = 1,
|
||||||
Duo = 2,
|
Duo = 2,
|
||||||
YubiKey = 3,
|
YubiKey = 3,
|
||||||
U2f = 4, // Deprecated
|
[Obsolete("Deprecated in favor of WebAuthn.")]
|
||||||
|
U2f = 4,
|
||||||
Remember = 5,
|
Remember = 5,
|
||||||
OrganizationDuo = 6,
|
OrganizationDuo = 6,
|
||||||
WebAuthn = 7,
|
WebAuthn = 7,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
{
|
{
|
||||||
private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}";
|
private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}";
|
||||||
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
private readonly IDistributedCache _distributedCache;
|
private readonly IDistributedCache _distributedCache;
|
||||||
private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;
|
private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions;
|
||||||
|
|
||||||
public AuthenticatorTokenProvider(
|
public AuthenticatorTokenProvider(
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
[FromKeyedServices("persistent")]
|
[FromKeyedServices("persistent")]
|
||||||
IDistributedCache distributedCache)
|
IDistributedCache distributedCache)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
_distributedCache = distributedCache;
|
_distributedCache = distributedCache;
|
||||||
_distributedCacheEntryOptions = new DistributedCacheEntryOptions
|
_distributedCacheEntryOptions = new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
@ -29,15 +25,14 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
|
var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
|
||||||
if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"]))
|
if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"]))
|
||||||
{
|
{
|
||||||
return false;
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
return await _serviceProvider.GetRequiredService<IUserService>()
|
return Task.FromResult(authenticatorProvider.Enabled);
|
||||||
.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||||
|
@ -16,10 +16,11 @@ public class DuoUniversalTokenProvider(
|
|||||||
IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider<User>
|
IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider<User>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance
|
/// We need the IServiceProvider to resolve the <see cref="IUserService"/>. There is a complex dependency dance
|
||||||
/// occurring between IUserService, which extends the UserManager<User>, and the usage of the
|
/// occurring between <see cref="IUserService"/>, which extends the <see cref="UserManager{User}"/>, and the usage
|
||||||
/// UserManager<User> within this class. Trying to resolve the IUserService using the DI pipeline
|
/// of the <see cref="UserManager{User}"/> within this class. Trying to resolve the <see cref="IUserService"/> using
|
||||||
/// will not allow the server to start and it will hang and give no helpful indication as to the problem.
|
/// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the
|
||||||
|
/// problem.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory = tokenDataFactory;
|
||||||
@ -28,12 +29,13 @@ public class DuoUniversalTokenProvider(
|
|||||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||||
var provider = await GetDuoTwoFactorProvider(user, userService);
|
var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService);
|
||||||
if (provider == null)
|
if (duoUniversalTokenProvider == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user);
|
|
||||||
|
return duoUniversalTokenProvider.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||||
@ -57,7 +59,7 @@ public class DuoUniversalTokenProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the Duo Two Factor Provider for the user if they have access to Duo
|
/// Get the Duo Two Factor Provider for the user if they have premium access to Duo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">Active User</param>
|
/// <param name="user">Active User</param>
|
||||||
/// <returns>null or Duo TwoFactorProvider</returns>
|
/// <returns>null or Duo TwoFactorProvider</returns>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders;
|
|||||||
|
|
||||||
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
public class EmailTwoFactorTokenProvider : EmailTokenProvider
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
|
|
||||||
public EmailTwoFactorTokenProvider(
|
public EmailTwoFactorTokenProvider(
|
||||||
IServiceProvider serviceProvider,
|
|
||||||
[FromKeyedServices("persistent")]
|
[FromKeyedServices("persistent")]
|
||||||
IDistributedCache distributedCache) :
|
IDistributedCache distributedCache) :
|
||||||
base(distributedCache)
|
base(distributedCache)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
|
||||||
|
|
||||||
TokenAlpha = false;
|
TokenAlpha = false;
|
||||||
TokenNumeric = true;
|
TokenNumeric = true;
|
||||||
TokenLength = 6;
|
TokenLength = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
|
||||||
if (!HasProperMetaData(provider))
|
if (!HasProperMetaData(emailTokenProvider))
|
||||||
{
|
{
|
||||||
return false;
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _serviceProvider.GetRequiredService<IUserService>().
|
return Task.FromResult(emailTokenProvider.Enabled);
|
||||||
TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
public override Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||||
|
@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
|
||||||
|
|
||||||
var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||||
|
// null check happens in this method
|
||||||
if (!HasProperMetaData(webAuthnProvider))
|
if (!HasProperMetaData(webAuthnProvider))
|
||||||
{
|
{
|
||||||
return false;
|
return Task.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user);
|
return Task.FromResult(webAuthnProvider.Enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
public async Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||||
@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn);
|
||||||
var keys = LoadKeys(provider);
|
var keys = LoadKeys(provider);
|
||||||
|
|
||||||
if (!provider.MetaData.ContainsKey("login"))
|
if (!provider.MetaData.TryGetValue("login", out var value))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,
|
var clientResponse = JsonSerializer.Deserialize<AuthenticatorAssertionRawResponse>(token,
|
||||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
|
||||||
var jsonOptions = provider.MetaData["login"].ToString();
|
var jsonOptions = value.ToString();
|
||||||
var options = AssertionOptions.FromJson(jsonOptions);
|
var options = AssertionOptions.FromJson(jsonOptions);
|
||||||
|
|
||||||
var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));
|
var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id));
|
||||||
@ -126,6 +125,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the provider has proper metadata.
|
||||||
|
/// This is used to determine if the provider has been properly configured.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider"></param>
|
||||||
|
/// <returns>true if metadata is present; false if empty or null</returns>
|
||||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||||
{
|
{
|
||||||
return provider?.MetaData?.Any() ?? false;
|
return provider?.MetaData?.Any() ?? false;
|
||||||
|
@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
|
|
||||||
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
|
||||||
{
|
{
|
||||||
|
// Ensure the user has access to premium
|
||||||
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
var userService = _serviceProvider.GetRequiredService<IUserService>();
|
||||||
if (!await userService.CanAccessPremium(user))
|
if (!await userService.CanAccessPremium(user))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);
|
// Check if the user has a YubiKey provider configured
|
||||||
if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)
|
var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey);
|
||||||
|
if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user);
|
return yubicoProvider.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
public Task<string> GenerateAsync(string purpose, UserManager<User> manager, User user)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -167,7 +167,7 @@ public class UserStore :
|
|||||||
|
|
||||||
public async Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken)
|
public async Task<bool> GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _serviceProvider.GetRequiredService<IUserService>().TwoFactorIsEnabledAsync(user);
|
return await _serviceProvider.GetRequiredService<ITwoFactorIsEnabledQuery>().TwoFactorIsEnabledAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken)
|
public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken)
|
||||||
|
@ -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;
|
namespace Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
|
||||||
// This token just provides a verifiable authN mechanism for the API service
|
/// <summary>
|
||||||
// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be
|
/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync
|
||||||
// used maliciously.
|
/// anonymous endpoint so it cannot used maliciously.
|
||||||
|
/// </summary>
|
||||||
public class SsoEmail2faSessionTokenable : ExpiringTokenable
|
public class SsoEmail2faSessionTokenable : ExpiringTokenable
|
||||||
{
|
{
|
||||||
// Just over 2 min expiration (client expires session after 2 min)
|
// Just over 2 min expiration (client expires session after 2 min)
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Models;
|
namespace Bit.Core.Auth.Models;
|
||||||
|
|
||||||
public interface ITwoFactorProvidersUser
|
public interface ITwoFactorProvidersUser
|
||||||
{
|
{
|
||||||
string TwoFactorProviders { get; }
|
string TwoFactorProviders { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Get the two factor providers for the user. Currently it can be assumed providers are enabled
|
||||||
|
/// if they exists in the dictionary. When two factor providers are disabled they are removed
|
||||||
|
/// from the dictionary. <see cref="IUserService.DisableTwoFactorProviderAsync"/>
|
||||||
|
/// <see cref="IOrganizationService.DisableTwoFactorProviderAsync"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Dictionary of providers with the type enum as the key</returns>
|
||||||
Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders();
|
Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders();
|
||||||
Guid? GetUserId();
|
Guid? GetUserId();
|
||||||
bool GetPremium();
|
bool GetPremium();
|
||||||
|
@ -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;
|
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
|
||||||
|
|
||||||
public interface ITwoFactorIsEnabledQuery
|
public interface ITwoFactorIsEnabledQuery
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery
|
|||||||
/// <typeparam name="T">The type of user in the list. Must implement <see cref="ITwoFactorProvidersUser"/>.</typeparam>
|
/// <typeparam name="T">The type of user in the list. Must implement <see cref="ITwoFactorProvidersUser"/>.</typeparam>
|
||||||
Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser;
|
Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether two factor is enabled for the user.
|
/// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium.
|
||||||
|
/// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="user">The user to check.</param>
|
/// <param name="user">The user to check.</param>
|
||||||
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Enums;
|
||||||
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth;
|
||||||
|
|
||||||
public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery
|
||||||
{
|
{
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository = userRepository;
|
||||||
|
|
||||||
public TwoFactorIsEnabledQuery(IUserRepository userRepository)
|
|
||||||
{
|
|
||||||
_userRepository = userRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
|
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
|
||||||
{
|
{
|
||||||
@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList());
|
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]);
|
||||||
|
|
||||||
foreach (var userDetail in userDetails)
|
foreach (var userDetail in userDetails)
|
||||||
{
|
{
|
||||||
var hasTwoFactor = false;
|
result.Add(
|
||||||
var providers = userDetail.GetTwoFactorProviders();
|
(userDetail.Id,
|
||||||
if (providers != null)
|
await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(),
|
||||||
{
|
() => Task.FromResult(userDetail.HasPremiumAccess))
|
||||||
// Get all enabled providers
|
)
|
||||||
var enabledProviderKeys = from provider in providers
|
);
|
||||||
where provider.Value?.Enabled ?? false
|
|
||||||
select provider.Key;
|
|
||||||
|
|
||||||
// Find the first provider that is enabled and passes the premium check
|
|
||||||
hasTwoFactor = enabledProviderKeys
|
|
||||||
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add((userDetail.Id, hasTwoFactor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var providers = user.GetTwoFactorProviders();
|
return await TwoFactorEnabledAsync(
|
||||||
if (providers == null || !providers.Any())
|
user.GetTwoFactorProviders(),
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value);
|
||||||
|
return calcUser?.HasPremiumAccess ?? false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks to see what kind of two-factor is enabled.
|
||||||
|
/// We use a delegate to check if the user has premium access, since there are multiple ways to
|
||||||
|
/// determine if a user has premium access.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providers">dictionary of two factor providers</param>
|
||||||
|
/// <param name="hasPremiumAccessDelegate">function to check if the user has premium access</param>
|
||||||
|
/// <returns> true if the user has two factor enabled; false otherwise;</returns>
|
||||||
|
private async static Task<bool> TwoFactorEnabledAsync(
|
||||||
|
Dictionary<TwoFactorProviderType, TwoFactorProvider> providers,
|
||||||
|
Func<Task<bool>> hasPremiumAccessDelegate)
|
||||||
|
{
|
||||||
|
// If there are no providers, then two factor is not enabled
|
||||||
|
if (providers == null || providers.Count == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all enabled providers
|
// Get all enabled providers
|
||||||
var enabledProviderKeys = providers
|
// TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.
|
||||||
.Where(provider => provider.Value?.Enabled ?? false)
|
var enabledProviderKeys = from provider in providers
|
||||||
.Select(provider => provider.Key);
|
where provider.Value?.Enabled ?? false
|
||||||
|
select provider.Key;
|
||||||
|
|
||||||
|
// If no providers are enabled then two factor is not enabled
|
||||||
if (!enabledProviderKeys.Any())
|
if (!enabledProviderKeys.Any())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if any enabled provider passes the premium check
|
// If there are only premium two factor options then standard two factor is not enabled
|
||||||
var hasTwoFactor = enabledProviderKeys
|
var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium);
|
||||||
.Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type))
|
if (onlyHasPremiumTwoFactor)
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
// If no enabled provider passes the check, check the repository for organization premium access
|
|
||||||
if (!hasTwoFactor)
|
|
||||||
{
|
{
|
||||||
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List<Guid> { userId.Value });
|
// There are no Standard two factor options, check if the user has premium access
|
||||||
var userDetail = userDetails.FirstOrDefault();
|
// If the user has premium access, then two factor is enabled
|
||||||
|
var premiumAccess = await hasPremiumAccessDelegate();
|
||||||
if (userDetail != null)
|
return premiumAccess;
|
||||||
{
|
|
||||||
hasTwoFactor = enabledProviderKeys
|
|
||||||
.Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type))
|
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasTwoFactor;
|
// The user has at least one non-premium two factor option
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,4 +7,5 @@ public class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEm
|
|||||||
{
|
{
|
||||||
public ProductTierType ProductTier { get; set; }
|
public ProductTierType ProductTier { get; set; }
|
||||||
public IEnumerable<ProductType> Products { get; set; }
|
public IEnumerable<ProductType> Products { get; set; }
|
||||||
|
public int? TrialLength { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.Auth.Models.Mail;
|
using Bit.Core.Auth.Models.Mail;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Models.Mail;
|
namespace Bit.Core.Billing.Models.Mail;
|
||||||
|
|
||||||
@ -16,13 +17,26 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail
|
|||||||
$"&email={Email}" +
|
$"&email={Email}" +
|
||||||
$"&fromEmail=true" +
|
$"&fromEmail=true" +
|
||||||
$"&productTier={(int)ProductTier}" +
|
$"&productTier={(int)ProductTier}" +
|
||||||
$"&product={string.Join(",", Product.Select(p => (int)p))}";
|
$"&product={string.Join(",", Product.Select(p => (int)p))}" +
|
||||||
|
$"&trialLength={TrialLength}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string VerifyYourEmailHTMLCopy =>
|
||||||
|
TrialLength == 7
|
||||||
|
? "Verify your email address below to finish signing up for your free trial."
|
||||||
|
: $"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan.";
|
||||||
|
|
||||||
|
public string VerifyYourEmailTextCopy =>
|
||||||
|
TrialLength == 7
|
||||||
|
? "Verify your email address using the link below and start your free trial of Bitwarden."
|
||||||
|
: $"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan.";
|
||||||
|
|
||||||
public ProductTierType ProductTier { get; set; }
|
public ProductTierType ProductTier { get; set; }
|
||||||
|
|
||||||
public IEnumerable<ProductType> Product { get; set; }
|
public IEnumerable<ProductType> Product { get; set; }
|
||||||
|
|
||||||
|
public int TrialLength { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Currently we only support one product type at a time, despite Product being a collection.
|
/// Currently we only support one product type at a time, despite Product being a collection.
|
||||||
/// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route
|
/// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route
|
||||||
|
@ -59,7 +59,7 @@ public interface IProviderBillingService
|
|||||||
int seatAdjustment);
|
int seatAdjustment);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the provided <paramref name="seatAdjustment"/> will result in a purchase for the <paramref name="provider"/>'s <see cref="planType"/>.
|
/// Determines whether the provided <paramref name="seatAdjustment"/> will result in a purchase for the <paramref name="provider"/>'s <see cref="PlanType"/>.
|
||||||
/// Seat adjustments that result in purchases include:
|
/// Seat adjustments that result in purchases include:
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item>The <paramref name="provider"/> going from below the seat minimum to above the seat minimum for the provided <paramref name="planType"/></item>
|
/// <item>The <paramref name="provider"/> going from below the seat minimum to above the seat minimum for the provided <paramref name="planType"/></item>
|
||||||
|
@ -5,14 +5,12 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Implementations;
|
namespace Bit.Core.Billing.Services.Implementations;
|
||||||
|
|
||||||
public class PaymentHistoryService(
|
public class PaymentHistoryService(
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ITransactionRepository transactionRepository,
|
ITransactionRepository transactionRepository) : IPaymentHistoryService
|
||||||
ILogger<PaymentHistoryService> logger) : IPaymentHistoryService
|
|
||||||
{
|
{
|
||||||
public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
|
public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
|
||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
|
@ -10,5 +10,6 @@ public interface ISendTrialInitiationEmailForRegistrationCommand
|
|||||||
string? name,
|
string? name,
|
||||||
bool receiveMarketingEmails,
|
bool receiveMarketingEmails,
|
||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products);
|
IEnumerable<ProductType> products,
|
||||||
|
int trialLength);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,8 @@ public class SendTrialInitiationEmailForRegistrationCommand(
|
|||||||
string? name,
|
string? name,
|
||||||
bool receiveMarketingEmails,
|
bool receiveMarketingEmails,
|
||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products)
|
IEnumerable<ProductType> products,
|
||||||
|
int trialLength)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email));
|
ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email));
|
||||||
|
|
||||||
@ -43,7 +44,12 @@ public class SendTrialInitiationEmailForRegistrationCommand(
|
|||||||
|
|
||||||
await PerformConstantTimeOperationsAsync();
|
await PerformConstantTimeOperationsAsync();
|
||||||
|
|
||||||
await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products);
|
if (trialLength != 0 && trialLength != 7)
|
||||||
|
{
|
||||||
|
trialLength = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -114,10 +114,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||||
public const string EmailVerification = "email-verification";
|
public const string EmailVerification = "email-verification";
|
||||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
|
||||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
|
||||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||||
public const string NewDeviceVerification = "new-device-verification";
|
|
||||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||||
@ -151,6 +148,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
|
||||||
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup";
|
||||||
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
public const string UseOrganizationWarningsService = "use-organization-warnings-service";
|
||||||
|
public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0";
|
||||||
|
|
||||||
/* Data Insights and Reporting Team */
|
/* Data Insights and Reporting Team */
|
||||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||||
@ -197,8 +195,6 @@ public static class FeatureFlagKeys
|
|||||||
/* Vault Team */
|
/* Vault Team */
|
||||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
|
|
||||||
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
|
|
||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string SecurityTasks = "security-tasks";
|
public const string SecurityTasks = "security-tasks";
|
||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
<GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
|
||||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
@ -77,4 +75,8 @@
|
|||||||
<Folder Include="Resources\" />
|
<Folder Include="Resources\" />
|
||||||
<Folder Include="Properties\" />
|
<Folder Include="Properties\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Infrastructure.IntegrationTest" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -128,6 +128,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
|
|
||||||
public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow;
|
public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Dictionary of TwoFactor providers</returns>
|
||||||
public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()
|
public Dictionary<TwoFactorProviderType, TwoFactorProvider>? GetTwoFactorProviders()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(TwoFactorProviders))
|
if (string.IsNullOrWhiteSpace(TwoFactorProviders))
|
||||||
@ -137,19 +141,17 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_twoFactorProviders == null)
|
_twoFactorProviders ??=
|
||||||
{
|
JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
|
||||||
_twoFactorProviders =
|
TwoFactorProviders);
|
||||||
JsonHelpers.LegacyDeserialize<Dictionary<TwoFactorProviderType, TwoFactorProvider>>(
|
|
||||||
TwoFactorProviders);
|
|
||||||
}
|
|
||||||
|
|
||||||
// U2F is no longer supported, and all users keys should have been migrated to WebAuthn.
|
/*
|
||||||
// To prevent issues with accounts being prompted for unsupported U2F we remove them
|
U2F is no longer supported, and all users keys should have been migrated to WebAuthn.
|
||||||
if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f))
|
To prevent issues with accounts being prompted for unsupported U2F we remove them.
|
||||||
{
|
This will probably exist in perpetuity since there is no way to know for sure if any
|
||||||
_twoFactorProviders.Remove(TwoFactorProviderType.U2f);
|
given user does or doesn't have this enabled. It is a non-zero chance.
|
||||||
}
|
*/
|
||||||
|
_twoFactorProviders?.Remove(TwoFactorProviderType.U2f);
|
||||||
|
|
||||||
return _twoFactorProviders;
|
return _twoFactorProviders;
|
||||||
}
|
}
|
||||||
@ -169,6 +171,10 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
return Premium;
|
return Premium;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="providers">Dictionary of Two Factor providers</param>
|
||||||
public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)
|
public void SetTwoFactorProviders(Dictionary<TwoFactorProviderType, TwoFactorProvider> providers)
|
||||||
{
|
{
|
||||||
// When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider.
|
// When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider.
|
||||||
@ -176,20 +182,21 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
_twoFactorProviders = providers;
|
_twoFactorProviders = providers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearTwoFactorProviders()
|
/// <summary>
|
||||||
{
|
/// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor
|
||||||
SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>());
|
/// configured it will still be found, even if the user's premium subscription has ended.
|
||||||
}
|
/// </summary>
|
||||||
|
/// <param name="provider">TwoFactor provider being searched for</param>
|
||||||
|
/// <returns>TwoFactorProvider if found; null otherwise.</returns>
|
||||||
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
|
public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider)
|
||||||
{
|
{
|
||||||
var providers = GetTwoFactorProviders();
|
var providers = GetTwoFactorProviders();
|
||||||
if (providers == null || !providers.ContainsKey(provider))
|
if (providers == null || !providers.TryGetValue(provider, out var value))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers[provider];
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long StorageBytesRemaining()
|
public long StorageBytesRemaining()
|
||||||
|
18
src/Core/Enums/EnumExtensions.cs
Normal file
18
src/Core/Enums/EnumExtensions.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
public static class EnumExtensions
|
||||||
|
{
|
||||||
|
public static string GetDisplayName(this Enum value)
|
||||||
|
{
|
||||||
|
var field = value.GetType().GetField(value.ToString());
|
||||||
|
if (field?.GetCustomAttribute<DisplayAttribute>() is { } attribute)
|
||||||
|
{
|
||||||
|
return attribute.Name ?? value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
}
|
@ -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}}
|
|
@ -2,7 +2,7 @@
|
|||||||
<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;">
|
<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;">
|
<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" align="center">
|
<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" align="center">
|
||||||
Verify your email address below to finish signing up for your free trial.
|
{{VerifyYourEmailHTMLCopy}}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr 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;">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{{#>BasicTextLayout}}
|
{{#>BasicTextLayout}}
|
||||||
Verify your email address using the link below and start your free trial of Bitwarden.
|
{{VerifyYourEmailTextCopy}}
|
||||||
|
|
||||||
If you did not request this email from Bitwarden, you can safely ignore it.
|
If you did not request this email from Bitwarden, you can safely ignore it.
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel
|
|||||||
public bool ToDelete { get; set; }
|
public bool ToDelete { get; set; }
|
||||||
|
|
||||||
public bool CloudSponsorshipRemoved { get; set; }
|
public bool CloudSponsorshipRemoved { get; set; }
|
||||||
|
public bool IsAdminInitiated { get; set; }
|
||||||
|
|
||||||
public OrganizationSponsorshipResponseModel() { }
|
public OrganizationSponsorshipResponseModel() { }
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel
|
|||||||
ValidUntil = sponsorshipData.ValidUntil;
|
ValidUntil = sponsorshipData.ValidUntil;
|
||||||
ToDelete = sponsorshipData.ToDelete;
|
ToDelete = sponsorshipData.ToDelete;
|
||||||
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
|
CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved;
|
||||||
|
IsAdminInitiated = sponsorshipData.IsAdminInitiated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OrganizationSponsorshipData ToOrganizationSponsorship()
|
public OrganizationSponsorshipData ToOrganizationSponsorship()
|
||||||
@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel
|
|||||||
LastSyncDate = LastSyncDate,
|
LastSyncDate = LastSyncDate,
|
||||||
ValidUntil = ValidUntil,
|
ValidUntil = ValidUntil,
|
||||||
ToDelete = ToDelete,
|
ToDelete = ToDelete,
|
||||||
CloudSponsorshipRemoved = CloudSponsorshipRemoved
|
CloudSponsorshipRemoved = CloudSponsorshipRemoved,
|
||||||
|
IsAdminInitiated = IsAdminInitiated,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
namespace Bit.Core.Models.Commands;
|
|
||||||
|
|
||||||
public class BadRequestFailure<T> : Failure<T>
|
|
||||||
{
|
|
||||||
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class BadRequestFailure : Failure
|
|
||||||
{
|
|
||||||
public BadRequestFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public BadRequestFailure(string errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using Bit.Core.AdminConsole.Errors;
|
|
||||||
using Bit.Core.AdminConsole.Shared.Validation;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Commands;
|
|
||||||
|
|
||||||
public class CommandResult(IEnumerable<string> errors)
|
|
||||||
{
|
|
||||||
public CommandResult(string error) : this([error]) { }
|
|
||||||
|
|
||||||
public bool Success => ErrorMessages.Count == 0;
|
|
||||||
public bool HasErrors => ErrorMessages.Count > 0;
|
|
||||||
public List<string> ErrorMessages { get; } = errors.ToList();
|
|
||||||
public CommandResult() : this(Array.Empty<string>()) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Failure : CommandResult
|
|
||||||
{
|
|
||||||
protected Failure(IEnumerable<string> errorMessages) : base(errorMessages)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
public Failure(string errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Success : CommandResult
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class CommandResult<T>;
|
|
||||||
|
|
||||||
public class Success<T>(T value) : CommandResult<T>
|
|
||||||
{
|
|
||||||
public T Value { get; } = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Failure<T>(IEnumerable<string> errorMessages) : CommandResult<T>
|
|
||||||
{
|
|
||||||
public List<string> ErrorMessages { get; } = errorMessages.ToList();
|
|
||||||
public Error<T>[] Errors { get; set; } = [];
|
|
||||||
|
|
||||||
public string ErrorMessage => string.Join(" ", ErrorMessages);
|
|
||||||
|
|
||||||
public Failure(string error) : this([error])
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Failure(IEnumerable<Error<T>> errors) : this(errors.Select(e => e.Message))
|
|
||||||
{
|
|
||||||
Errors = errors.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Failure(Error<T> error) : this([error.Message])
|
|
||||||
{
|
|
||||||
Errors = [error];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class Partial<T> : CommandResult<T>
|
|
||||||
{
|
|
||||||
public T[] Successes { get; set; } = [];
|
|
||||||
public Error<T>[] Failures { get; set; } = [];
|
|
||||||
|
|
||||||
public Partial(IEnumerable<T> successfulItems, IEnumerable<Error<T>> failedItems)
|
|
||||||
{
|
|
||||||
Successes = successfulItems.ToArray();
|
|
||||||
Failures = failedItems.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CommandResultExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types.
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="invalidResult">This is the invalid type from validating the object.</param>
|
|
||||||
/// <param name="mappingFunction">This function will map between the two types for the inner ErrorT</param>
|
|
||||||
/// <typeparam name="A">Invalid object's type</typeparam>
|
|
||||||
/// <typeparam name="B">Failure object's type</typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static CommandResult<B> MapToFailure<A, B>(this Invalid<A> invalidResult, Func<A, B> mappingFunction) =>
|
|
||||||
new Failure<B>(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue))));
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
namespace Bit.Core.Models.Commands;
|
|
||||||
|
|
||||||
public class NoRecordFoundFailure<T> : Failure<T>
|
|
||||||
{
|
|
||||||
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class NoRecordFoundFailure : Failure
|
|
||||||
{
|
|
||||||
public NoRecordFoundFailure(IEnumerable<string> errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public NoRecordFoundFailure(string errorMessage) : base(errorMessage)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -25,6 +25,16 @@ public interface IUserRepository : IRepository<User, Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);
|
Task<IEnumerable<UserWithCalculatedPremium>> GetManyWithCalculatedPremiumAsync(IEnumerable<Guid> ids);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Retrieves the data for the requested user ID and includes additional property indicating
|
||||||
|
/// whether the user has premium access directly or through an organization.
|
||||||
|
///
|
||||||
|
/// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query
|
||||||
|
/// for a single user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user ID to retrieve data for.</param>
|
||||||
|
/// <returns>User data with calculated premium access; null if nothing is found</returns>
|
||||||
|
Task<UserWithCalculatedPremium?> GetCalculatedPremiumAsync(Guid userId);
|
||||||
|
/// <summary>
|
||||||
/// Sets a new user key and updates all encrypted data.
|
/// Sets a new user key and updates all encrypted data.
|
||||||
/// <para>Warning: Any user key encrypted data not included will be lost.</para>
|
/// <para>Warning: Any user key encrypted data not included will be lost.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -21,7 +21,8 @@ public interface IMailService
|
|||||||
string email,
|
string email,
|
||||||
string token,
|
string token,
|
||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products);
|
IEnumerable<ProductType> products,
|
||||||
|
int trialLength);
|
||||||
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
||||||
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
|
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
|
||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
@ -87,8 +88,6 @@ public interface IMailService
|
|||||||
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail);
|
||||||
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate);
|
||||||
Task SendOTPEmailAsync(string email, string token);
|
Task SendOTPEmailAsync(string email, string token);
|
||||||
Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
|
||||||
Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip);
|
|
||||||
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||||
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName);
|
||||||
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
|
@ -71,11 +71,13 @@ public interface IUserService
|
|||||||
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
|
Task<UserLicense> GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null,
|
||||||
int? version = null);
|
int? version = null);
|
||||||
Task<bool> CheckPasswordAsync(User user, string password);
|
Task<bool> CheckPasswordAsync(User user, string password);
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the user has access to premium features, either through a personal subscription or through an organization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">user being acted on</param>
|
||||||
|
/// <returns>true if they can access premium; false otherwise.</returns>
|
||||||
Task<bool> CanAccessPremium(ITwoFactorProvidersUser user);
|
Task<bool> CanAccessPremium(ITwoFactorProvidersUser user);
|
||||||
Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user);
|
Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user);
|
||||||
[Obsolete("Use ITwoFactorIsEnabledQuery instead.")]
|
|
||||||
Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user);
|
|
||||||
Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user);
|
|
||||||
Task<string> GenerateSignInTokenAsync(User user, string purpose);
|
Task<string> GenerateSignInTokenAsync(User user, string purpose);
|
||||||
|
|
||||||
Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
|
Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
|
||||||
|
@ -84,7 +84,8 @@ public class HandlebarsMailService : IMailService
|
|||||||
string email,
|
string email,
|
||||||
string token,
|
string token,
|
||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products)
|
IEnumerable<ProductType> products,
|
||||||
|
int trialLength)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Verify your email", email);
|
var message = CreateDefaultMessage("Verify your email", email);
|
||||||
var model = new TrialInitiationVerifyEmail
|
var model = new TrialInitiationVerifyEmail
|
||||||
@ -95,7 +96,8 @@ public class HandlebarsMailService : IMailService
|
|||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
SiteName = _globalSettings.SiteName,
|
SiteName = _globalSettings.SiteName,
|
||||||
ProductTier = productTier,
|
ProductTier = productTier,
|
||||||
Product = products
|
Product = products,
|
||||||
|
TrialLength = trialLength
|
||||||
};
|
};
|
||||||
await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model);
|
await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model);
|
||||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||||
@ -1135,40 +1137,6 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
|
|
||||||
{
|
|
||||||
var message = CreateDefaultMessage("Failed login attempts detected", email);
|
|
||||||
var model = new FailedAuthAttemptsModel()
|
|
||||||
{
|
|
||||||
TheDate = utcNow.ToLongDateString(),
|
|
||||||
TheTime = utcNow.ToShortTimeString(),
|
|
||||||
TimeZone = _utcTimeZoneDisplay,
|
|
||||||
IpAddress = ip,
|
|
||||||
AffectedEmail = email
|
|
||||||
|
|
||||||
};
|
|
||||||
await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model);
|
|
||||||
message.Category = "FailedLoginAttempts";
|
|
||||||
await _mailDeliveryService.SendEmailAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
|
|
||||||
{
|
|
||||||
var message = CreateDefaultMessage("Failed login attempts detected", email);
|
|
||||||
var model = new FailedAuthAttemptsModel()
|
|
||||||
{
|
|
||||||
TheDate = utcNow.ToLongDateString(),
|
|
||||||
TheTime = utcNow.ToShortTimeString(),
|
|
||||||
TimeZone = _utcTimeZoneDisplay,
|
|
||||||
IpAddress = ip,
|
|
||||||
AffectedEmail = email
|
|
||||||
|
|
||||||
};
|
|
||||||
await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model);
|
|
||||||
message.Category = "FailedTwoFactorAttempts";
|
|
||||||
await _mailDeliveryService.SendEmailAsync(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("Domain not verified", adminEmails);
|
var message = CreateDefaultMessage("Domain not verified", adminEmails);
|
||||||
|
@ -112,6 +112,8 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes.");
|
throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingCoupon = sub.Customer.Discount?.Coupon?.Id;
|
||||||
|
|
||||||
var collectionMethod = sub.CollectionMethod;
|
var collectionMethod = sub.CollectionMethod;
|
||||||
var daysUntilDue = sub.DaysUntilDue;
|
var daysUntilDue = sub.DaysUntilDue;
|
||||||
var chargeNow = collectionMethod == "charge_automatically";
|
var chargeNow = collectionMethod == "charge_automatically";
|
||||||
@ -216,6 +218,19 @@ public class StripePaymentService : IPaymentService
|
|||||||
DaysUntilDue = daysUntilDue,
|
DaysUntilDue = daysUntilDue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId);
|
||||||
|
|
||||||
|
var newCoupon = customer.Discount?.Coupon?.Id;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon))
|
||||||
|
{
|
||||||
|
// Re-add the lost coupon due to the update.
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Coupon = existingCoupon
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paymentIntentClientSecret;
|
return paymentIntentClientSecret;
|
||||||
|
@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Sales;
|
using Bit.Core.Billing.Models.Sales;
|
||||||
@ -77,6 +78,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
private readonly IPremiumUserBillingService _premiumUserBillingService;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||||
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IDistributedCache _distributedCache;
|
private readonly IDistributedCache _distributedCache;
|
||||||
|
|
||||||
public UserService(
|
public UserService(
|
||||||
@ -115,6 +117,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
IPremiumUserBillingService premiumUserBillingService,
|
IPremiumUserBillingService premiumUserBillingService,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IDistributedCache distributedCache)
|
IDistributedCache distributedCache)
|
||||||
: base(
|
: base(
|
||||||
store,
|
store,
|
||||||
@ -158,6 +161,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
_premiumUserBillingService = premiumUserBillingService;
|
_premiumUserBillingService = premiumUserBillingService;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||||
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_distributedCache = distributedCache;
|
_distributedCache = distributedCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -918,7 +922,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
await SaveUserAsync(user);
|
await SaveUserAsync(user);
|
||||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa);
|
||||||
|
|
||||||
if (!await TwoFactorIsEnabledAsync(user))
|
if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user))
|
||||||
{
|
{
|
||||||
await CheckPoliciesOnTwoFactorRemovalAsync(user);
|
await CheckPoliciesOnTwoFactorRemovalAsync(user);
|
||||||
}
|
}
|
||||||
@ -1280,48 +1284,6 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
orgAbility.UsersGetPremium &&
|
orgAbility.UsersGetPremium &&
|
||||||
orgAbility.Enabled);
|
orgAbility.Enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
|
|
||||||
{
|
|
||||||
var providers = user.GetTwoFactorProviders();
|
|
||||||
if (providers == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var p in providers)
|
|
||||||
{
|
|
||||||
if (p.Value?.Enabled ?? false)
|
|
||||||
{
|
|
||||||
if (!TwoFactorProvider.RequiresPremium(p.Key))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (await CanAccessPremium(user))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user)
|
|
||||||
{
|
|
||||||
var providers = user.GetTwoFactorProviders();
|
|
||||||
if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TwoFactorProvider.RequiresPremium(provider))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await CanAccessPremium(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<string> GenerateSignInTokenAsync(User user, string purpose)
|
public async Task<string> GenerateSignInTokenAsync(User user, string purpose)
|
||||||
{
|
{
|
||||||
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider,
|
||||||
|
@ -33,7 +33,8 @@ public class NoopMailService : IMailService
|
|||||||
string email,
|
string email,
|
||||||
string token,
|
string token,
|
||||||
ProductTierType productTier,
|
ProductTierType productTier,
|
||||||
IEnumerable<ProductType> products)
|
IEnumerable<ProductType> products,
|
||||||
|
int trailLength)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
@ -267,16 +268,6 @@ public class NoopMailService : IMailService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable<string> adminEmails, string organizationId, string domainName)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual bool EnableCloudCommunication { get; set; } = false;
|
public virtual bool EnableCloudCommunication { get; set; } = false;
|
||||||
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
|
public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days
|
||||||
public virtual string EventGridKey { get; set; }
|
public virtual string EventGridKey { get; set; }
|
||||||
public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings();
|
|
||||||
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
|
public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings();
|
||||||
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
|
public virtual IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||||
public virtual string DatabaseProvider { get; set; }
|
public virtual string DatabaseProvider { get; set; }
|
||||||
@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public bool EnforceSsoPolicyForAllUsers { get; set; }
|
public bool EnforceSsoPolicyForAllUsers { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CaptchaSettings
|
|
||||||
{
|
|
||||||
public bool ForceCaptchaRequired { get; set; } = false;
|
|
||||||
public string HCaptchaSecretKey { get; set; }
|
|
||||||
public string HCaptchaSiteKey { get; set; }
|
|
||||||
public int MaximumFailedLoginAttempts { get; set; }
|
|
||||||
public double MaybeBotScoreThreshold { get; set; } = double.MaxValue;
|
|
||||||
public double IsBotScoreThreshold { get; set; } = double.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StripeSettings
|
public class StripeSettings
|
||||||
{
|
{
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
|
|||||||
cipher.Value.ViewPassword = true;
|
cipher.Value.ViewPassword = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (await CanAccessUnassignedCiphersAsync(org))
|
else if (CanAccessUnassignedCiphers(org))
|
||||||
{
|
{
|
||||||
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
|
var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId);
|
||||||
foreach (var unassignedCipher in unassignedCiphers)
|
foreach (var unassignedCipher in unassignedCiphers)
|
||||||
@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CanAccessUnassignedCiphersAsync(CurrentContextOrganization org)
|
private bool CanAccessUnassignedCiphers(CurrentContextOrganization org)
|
||||||
{
|
{
|
||||||
if (org is
|
if (org is
|
||||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Queries;
|
||||||
|
|
||||||
|
|
||||||
namespace Bit.Core.Vault.Repositories;
|
namespace Bit.Core.Vault.Repositories;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||||
using Bit.Core.Billing.TrialInitiation.Registration;
|
using Bit.Core.Billing.TrialInitiation.Registration;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
@ -15,18 +17,24 @@ namespace Bit.Identity.Billing.Controller;
|
|||||||
public class AccountsController(
|
public class AccountsController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand,
|
ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand,
|
||||||
IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller
|
IReferenceEventService referenceEventService,
|
||||||
|
IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller
|
||||||
{
|
{
|
||||||
[HttpPost("trial/send-verification-email")]
|
[HttpPost("trial/send-verification-email")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
|
public async Task<IActionResult> PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model)
|
||||||
{
|
{
|
||||||
|
var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0);
|
||||||
|
|
||||||
|
var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7;
|
||||||
|
|
||||||
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
|
var token = await sendTrialInitiationEmailForRegistrationCommand.Handle(
|
||||||
model.Email,
|
model.Email,
|
||||||
model.Name,
|
model.Name,
|
||||||
model.ReceiveMarketingEmails,
|
model.ReceiveMarketingEmails,
|
||||||
model.ProductTier,
|
model.ProductTier,
|
||||||
model.Products);
|
model.Products,
|
||||||
|
trialLength);
|
||||||
|
|
||||||
var refEvent = new ReferenceEvent
|
var refEvent = new ReferenceEvent
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Services;
|
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -37,7 +36,6 @@ public class AccountsController : Controller
|
|||||||
private readonly ILogger<AccountsController> _logger;
|
private readonly ILogger<AccountsController> _logger;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
private readonly IRegisterUserCommand _registerUserCommand;
|
private readonly IRegisterUserCommand _registerUserCommand;
|
||||||
private readonly ICaptchaValidationService _captchaValidationService;
|
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand;
|
||||||
@ -85,7 +83,6 @@ public class AccountsController : Controller
|
|||||||
ILogger<AccountsController> logger,
|
ILogger<AccountsController> logger,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IRegisterUserCommand registerUserCommand,
|
IRegisterUserCommand registerUserCommand,
|
||||||
ICaptchaValidationService captchaValidationService,
|
|
||||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||||
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand,
|
||||||
@ -99,7 +96,6 @@ public class AccountsController : Controller
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_registerUserCommand = registerUserCommand;
|
_registerUserCommand = registerUserCommand;
|
||||||
_captchaValidationService = captchaValidationService;
|
|
||||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
_sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand;
|
||||||
@ -167,7 +163,7 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register/finish")]
|
[HttpPost("register/finish")]
|
||||||
public async Task<RegisterResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
public async Task<RegisterFinishResponseModel> PostRegisterFinish([FromBody] RegisterFinishRequestModel model)
|
||||||
{
|
{
|
||||||
var user = model.ToUser();
|
var user = model.ToUser();
|
||||||
|
|
||||||
@ -208,12 +204,11 @@ public class AccountsController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user)
|
private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user)
|
||||||
{
|
{
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user);
|
return new RegisterFinishResponseModel();
|
||||||
return new RegisterResponseModel(captchaBypassToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName"))
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<UserSecretsId>bitwarden-Identity</UserSecretsId>
|
<UserSecretsId>bitwarden-Identity</UserSecretsId>
|
||||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||||
<!-- Temp exclusions until warnings are fixed -->
|
|
||||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0162</WarningsNotAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Identity' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Identity' " />
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.Auth.Models.Business;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Entities;
|
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
namespace Bit.Identity.IdentityServer;
|
namespace Bit.Identity.IdentityServer;
|
||||||
@ -9,7 +8,7 @@ public class CustomValidatorRequestContext
|
|||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is the device that the user is using to authenticate. It can be either known or unknown.
|
/// This is the device that the user is using to authenticate. It can be either known or unknown.
|
||||||
/// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required.
|
/// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation.
|
||||||
/// The option to set it here saves a trip to the database.
|
/// The option to set it here saves a trip to the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Device Device { get; set; }
|
public Device Device { get; set; }
|
||||||
@ -39,5 +38,4 @@ public class CustomValidatorRequestContext
|
|||||||
/// This will be null if the authentication request is successful.
|
/// This will be null if the authentication request is successful.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, object> CustomResponse { get; set; }
|
public Dictionary<string, object> CustomResponse { get; set; }
|
||||||
public CaptchaResponse CaptchaResponse { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IMailService _mailService;
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
@ -49,7 +48,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -66,7 +64,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_mailService = mailService;
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
CurrentContext = currentContext;
|
CurrentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
@ -81,23 +78,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
CustomValidatorRequestContext validatorContext)
|
||||||
{
|
{
|
||||||
// 1. We need to check if the user is a bot and if their master password hash is correct.
|
// 1. We need to check if the user's master password hash is correct.
|
||||||
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
|
|
||||||
var valid = await ValidateContextAsync(context, validatorContext);
|
var valid = await ValidateContextAsync(context, validatorContext);
|
||||||
var user = validatorContext.User;
|
var user = validatorContext.User;
|
||||||
if (!valid || isBot)
|
if (!valid)
|
||||||
{
|
{
|
||||||
if (isBot)
|
await UpdateFailedAuthDetailsAsync(user);
|
||||||
{
|
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
|
||||||
"Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.",
|
|
||||||
request.UserName, validatorContext.CaptchaResponse.Score);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!valid)
|
|
||||||
{
|
|
||||||
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||||
return;
|
return;
|
||||||
@ -167,7 +153,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice);
|
await UpdateFailedAuthDetailsAsync(user);
|
||||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -379,7 +365,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice)
|
private async Task UpdateFailedAuthDetailsAsync(User user)
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@ -390,32 +376,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
user.FailedLoginCount = ++user.FailedLoginCount;
|
user.FailedLoginCount = ++user.FailedLoginCount;
|
||||||
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
user.LastFailedLoginDate = user.RevisionDate = utcNow;
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
|
|
||||||
if (ValidateFailedAuthEmailConditions(unknownDevice, user))
|
|
||||||
{
|
|
||||||
if (twoFactorInvalid)
|
|
||||||
{
|
|
||||||
await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// checks to see if a user is trying to log into a new device
|
|
||||||
/// and has reached the maximum number of failed login attempts.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="unknownDevice">boolean</param>
|
|
||||||
/// <param name="user">current user</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
|
|
||||||
{
|
|
||||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
|
||||||
var failedLoginCount = user?.FailedLoginCount ?? 0;
|
|
||||||
return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
|
private async Task<MasterPasswordPolicyResponseModel> GetMasterPasswordPolicyAsync(User user)
|
||||||
|
@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IMailService mailService,
|
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
mailService,
|
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
|
@ -22,8 +22,7 @@ public class DeviceValidator(
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IDistributedCache distributedCache,
|
IDistributedCache distributedCache,
|
||||||
ILogger<DeviceValidator> logger,
|
ILogger<DeviceValidator> logger) : IDeviceValidator
|
||||||
IFeatureService featureService) : IDeviceValidator
|
|
||||||
{
|
{
|
||||||
private readonly IDeviceService _deviceService = deviceService;
|
private readonly IDeviceService _deviceService = deviceService;
|
||||||
private readonly IDeviceRepository _deviceRepository = deviceRepository;
|
private readonly IDeviceRepository _deviceRepository = deviceRepository;
|
||||||
@ -33,7 +32,6 @@ public class DeviceValidator(
|
|||||||
private readonly IUserService _userService = userService;
|
private readonly IUserService _userService = userService;
|
||||||
private readonly IDistributedCache distributedCache = distributedCache;
|
private readonly IDistributedCache distributedCache = distributedCache;
|
||||||
private readonly ILogger<DeviceValidator> _logger = logger;
|
private readonly ILogger<DeviceValidator> _logger = logger;
|
||||||
private readonly IFeatureService _featureService = featureService;
|
|
||||||
|
|
||||||
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
||||||
{
|
{
|
||||||
@ -64,9 +62,7 @@ public class DeviceValidator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We have established that the device is unknown at this point; begin new device verification
|
// We have established that the device is unknown at this point; begin new device verification
|
||||||
// PM-13340: remove feature flag
|
if (request.GrantType == "password" &&
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) &&
|
|
||||||
request.GrantType == "password" &&
|
|
||||||
request.Raw["AuthRequest"] == null &&
|
request.Raw["AuthRequest"] == null &&
|
||||||
!context.TwoFactorRequired &&
|
!context.TwoFactorRequired &&
|
||||||
!context.SsoRequired &&
|
!context.SsoRequired &&
|
||||||
|
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