mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
Merge branch 'master' into EC-343-server-gate-custom-permissions-behind-enterprise
This commit is contained in:
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -71,3 +71,11 @@ body:
|
||||
- Operating system: [e.g. Windows 10, Mac OS Catalina]
|
||||
- Environment: [e.g. Docker, EKS, ECS, K8S]
|
||||
- Hardware: [e.g. Intel 6-core, 8GB RAM]
|
||||
- type: checkboxes
|
||||
id: issue-tracking-info
|
||||
attributes:
|
||||
label: Issue Tracking Info
|
||||
description: |
|
||||
Issue tracking information
|
||||
options:
|
||||
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.
|
||||
|
24
.github/workflows/version-bump.yml
vendored
24
.github/workflows/version-bump.yml
vendored
@ -16,6 +16,26 @@ jobs:
|
||||
- name: Checkout Branch
|
||||
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
|
||||
|
||||
- name: Login to Azure - Prod Subscription
|
||||
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
|
||||
with:
|
||||
keyvault: "bitwarden-prod-kv"
|
||||
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Create Version Branch
|
||||
run: |
|
||||
git switch -c version_bump_${{ github.event.inputs.version_number }}
|
||||
@ -28,8 +48,8 @@ jobs:
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
|
||||
- name: Check if version changed
|
||||
id: version-changed
|
||||
|
@ -98,6 +98,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{EC2D422A-6060-48E2-AAD2-37220D759F03}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroBenchmarks", "perf\MicroBenchmarks\MicroBenchmarks.csproj", "{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
|
||||
EndProject
|
||||
Global
|
||||
@ -232,14 +236,18 @@ Global
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -280,8 +288,9 @@ Global
|
||||
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
|
||||
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61} = {EC2D422A-6060-48E2-AAD2-37220D759F03}
|
||||
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
|
@ -6,12 +6,44 @@
|
||||
# in the future and investigate if we can migrate back.
|
||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
||||
|
||||
docker run `
|
||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
||||
--env-file .env `
|
||||
--network=bitwardenserver_default `
|
||||
--rm `
|
||||
mcr.microsoft.com/mssql-tools `
|
||||
/mnt/helpers/run_migrations.sh @args
|
||||
param([switch]$all = $false, [switch]$postgres = $false, [switch]$mysql = $false, [switch]$mssql = $false)
|
||||
|
||||
if (!$all -and !$postgres -and !$mysql) {
|
||||
$mssql = $true;
|
||||
}
|
||||
|
||||
if ($all -or $postgres -or $mysql) {
|
||||
dotnet ef *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install"
|
||||
dotnet tool install dotnet-ef -g
|
||||
}
|
||||
}
|
||||
|
||||
if ($all -or $mssql) {
|
||||
Write-Host "Starting Microsoft SQL Server Migrations"
|
||||
docker run `
|
||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
||||
--env-file .env `
|
||||
--network=bitwardenserver_default `
|
||||
--rm `
|
||||
mcr.microsoft.com/mssql-tools `
|
||||
/mnt/helpers/run_migrations.sh @args
|
||||
}
|
||||
|
||||
$currentDir = Get-Location
|
||||
|
||||
if ($all -or $mysql) {
|
||||
Write-Host "Starting MySQL Migrations"
|
||||
Set-Location "$currentDir/../util/MySqlMigrations/"
|
||||
dotnet ef database update
|
||||
}
|
||||
if ($all -or $postgres) {
|
||||
Write-Host "Starting PostgreSQL Migrations"
|
||||
Set-Location "$currentDir/../util/PostgresMigrations/"
|
||||
dotnet ef database update
|
||||
}
|
||||
|
||||
Set-Location "$currentDir"
|
||||
|
23
perf/MicroBenchmarks/Core/EncryptedStringAttributeTests.cs
Normal file
23
perf/MicroBenchmarks/Core/EncryptedStringAttributeTests.cs
Normal file
File diff suppressed because one or more lines are too long
18
perf/MicroBenchmarks/MicroBenchmarks.csproj
Normal file
18
perf/MicroBenchmarks/MicroBenchmarks.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
4
perf/MicroBenchmarks/Program.cs
Normal file
4
perf/MicroBenchmarks/Program.cs
Normal file
@ -0,0 +1,4 @@
|
||||
using System.Reflection;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
BenchmarkRunner.Run(Assembly.GetEntryAssembly());
|
2668
perf/MicroBenchmarks/packages.lock.json
Normal file
2668
perf/MicroBenchmarks/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -24,8 +24,8 @@
|
||||
<option asp-selected="@(Model.Project == "Identity")" value="Identity">Identity</option>
|
||||
<option asp-selected="@(Model.Project == "Notifications")" value="Notifications">Notifications</option>
|
||||
<option asp-selected="@(Model.Project == "Icons")" value="Icons">Icons</option>
|
||||
<option asp-selected="@(Model.Project == "Business Portal")" value="Business Portal">Business Portal</option>
|
||||
<option asp-selected="@(Model.Project == "SSO")" value="SSO">SSO</option>
|
||||
<option asp-selected="@(Model.Project == "Scim")" value="Scim">SCIM</option>
|
||||
</select>
|
||||
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="Start" name="start" placeholder="Start Date">
|
||||
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="End" name="end" placeholder="End Date">
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -28,6 +29,7 @@ public class TwoFactorController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
|
||||
|
||||
public TwoFactorController(
|
||||
IUserService userService,
|
||||
@ -35,7 +37,8 @@ public class TwoFactorController : Controller
|
||||
IOrganizationService organizationService,
|
||||
GlobalSettings globalSettings,
|
||||
UserManager<User> userManager,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IVerifyAuthRequestCommand verifyAuthRequestCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -43,6 +46,7 @@ public class TwoFactorController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
_verifyAuthRequestCommand = verifyAuthRequestCommand;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -285,19 +289,27 @@ public class TwoFactorController : Controller
|
||||
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
|
||||
if (user != null)
|
||||
{
|
||||
if (await _userService.VerifySecretAsync(user, model.Secret))
|
||||
// check if 2FA email is from passwordless
|
||||
if (!string.IsNullOrEmpty(model.AuthRequestAccessCode))
|
||||
{
|
||||
var isBecauseNewDeviceLogin = false;
|
||||
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
|
||||
&&
|
||||
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
|
||||
if (await _verifyAuthRequestCommand
|
||||
.VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode))
|
||||
{
|
||||
model.ToUser(user);
|
||||
isBecauseNewDeviceLogin = true;
|
||||
}
|
||||
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
|
||||
|
||||
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
|
||||
return;
|
||||
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (await _userService.VerifySecretAsync(user, model.Secret))
|
||||
{
|
||||
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
|
||||
|
||||
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -455,4 +467,17 @@ public class TwoFactorController : Controller
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsNewDeviceLoginAsync(User user, TwoFactorEmailRequestModel model)
|
||||
{
|
||||
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
|
||||
&&
|
||||
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
|
||||
{
|
||||
model.ToUser(user);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ public class KdfRequestModel : PasswordRequestModel, IValidatableObject
|
||||
[Required]
|
||||
public int? KdfIterations { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (Kdf.HasValue && KdfIterations.HasValue)
|
||||
{
|
||||
|
@ -7,13 +7,14 @@ public class SecretVerificationRequestModel : IValidatableObject
|
||||
[StringLength(300)]
|
||||
public string MasterPasswordHash { get; set; }
|
||||
public string OTP { get; set; }
|
||||
public string AuthRequestAccessCode { get; set; }
|
||||
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
|
||||
|
||||
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Secret))
|
||||
if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode))
|
||||
{
|
||||
yield return new ValidationResult("MasterPasswordHash or OTP must be supplied.");
|
||||
yield return new ValidationResult("MasterPasswordHash, OTP or AccessCode must be supplied.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
|
||||
return extistingOrg;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (!Core.Utilities.Duo.DuoApi.ValidHost(Host))
|
||||
{
|
||||
@ -160,7 +160,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod
|
||||
return keyValue.Substring(0, 12);
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Key1) && string.IsNullOrWhiteSpace(Key2) && string.IsNullOrWhiteSpace(Key3) &&
|
||||
string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5))
|
||||
@ -204,6 +204,8 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
|
||||
|
||||
public string DeviceIdentifier { get; set; }
|
||||
|
||||
public string AuthRequestId { get; set; }
|
||||
|
||||
public User ToUser(User extistingUser)
|
||||
{
|
||||
var providers = extistingUser.GetTwoFactorProviders();
|
||||
|
@ -1,5 +1,7 @@
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
// If the backing type here changes to a different type you will likely also need to change the value used in
|
||||
// EncryptedStringAttribute
|
||||
public enum EncryptionType : byte
|
||||
{
|
||||
AesCbc256_B64 = 0,
|
||||
|
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal file
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.LoginFeatures.PasswordlessLogin;
|
||||
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.LoginFeatures;
|
||||
|
||||
public static class LoginServiceCollectionExtensions
|
||||
{
|
||||
public static void AddLoginServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IVerifyAuthRequestCommand, VerifyAuthRequestCommand>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
|
||||
public interface IVerifyAuthRequestCommand
|
||||
{
|
||||
Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.LoginFeatures.PasswordlessLogin;
|
||||
|
||||
public class VerifyAuthRequestCommand : IVerifyAuthRequestCommand
|
||||
{
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
|
||||
public VerifyAuthRequestCommand(IAuthRequestRepository authRequestRepository)
|
||||
{
|
||||
_authRequestRepository = authRequestRepository;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
|
||||
if (authRequest == null || authRequest.AccessCode != accessCode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -54,16 +54,19 @@ public abstract class BaseIdentityClientService : IDisposable
|
||||
|
||||
protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path, TRequest requestModel)
|
||||
{
|
||||
var fullRequestPath = string.Concat(Client.BaseAddress, path);
|
||||
|
||||
var tokenStateResponse = await HandleTokenStateAsync();
|
||||
if (!tokenStateResponse)
|
||||
{
|
||||
_logger.LogError("Unable to send {method} request to {requestUri} because an access token was unable to be obtained", method.Method, fullRequestPath);
|
||||
return default;
|
||||
}
|
||||
|
||||
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
|
||||
{
|
||||
Method = method,
|
||||
RequestUri = new Uri(string.Concat(Client.BaseAddress, path))
|
||||
RequestUri = new Uri(fullRequestPath)
|
||||
};
|
||||
try
|
||||
{
|
||||
@ -120,7 +123,7 @@ public abstract class BaseIdentityClientService : IDisposable
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogInformation("Unsuccessful token response with status code {StatusCode}", response.StatusCode);
|
||||
_logger.LogInformation("Unsuccessful token response from {identity} for client {clientId} with status code {StatusCode}", IdentityClient.BaseAddress, _identityClientId, response.StatusCode);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.BadRequest)
|
||||
{
|
||||
|
@ -44,7 +44,7 @@ public class MultiServicePushNotificationService : IPushNotificationService
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
|
||||
{
|
||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
||||
globalSettings, httpContextAccessor));
|
||||
globalSettings, httpContextAccessor, hubLogger));
|
||||
}
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
@ -17,20 +18,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
private NotificationHubClient _client = null;
|
||||
private ILogger _logger;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationsApiPushNotificationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
_globalSettings.NotificationHub.ConnectionString,
|
||||
_globalSettings.NotificationHub.HubName);
|
||||
_globalSettings.NotificationHub.HubName,
|
||||
_globalSettings.NotificationHub.EnableSendTracing);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||
@ -244,12 +248,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
|
||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||
{
|
||||
await _client.SendTemplateNotificationAsync(
|
||||
var outcome = await _client.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
if (_globalSettings.NotificationHub.EnableSendTracing)
|
||||
{
|
||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||
}
|
||||
}
|
||||
|
||||
private string SanitizeTagInput(string input)
|
||||
|
@ -412,6 +412,12 @@ public class GlobalSettings : IGlobalSettings
|
||||
set => _connectionString = value.Trim('"');
|
||||
}
|
||||
public string HubName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables TestSend on the Azure Notification Hub, which allows tracing of the request through the hub and to the platform-specific push notification service (PNS).
|
||||
/// Enabling this will result in delayed responses because the Hub must wait on delivery to the PNS. This should ONLY be enabled in a non-production environment, as results are throttled.
|
||||
/// </summary>
|
||||
public bool EnableSendTracing { get; set; } = false;
|
||||
}
|
||||
|
||||
public class YubicoSettings
|
||||
|
@ -23,7 +23,6 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
|
||||
/// Unprotect token
|
||||
/// </summary>
|
||||
/// <param name="token">The token to parse</param>
|
||||
/// <typeparam name="T">The tokenable type to parse to</typeparam>
|
||||
/// <returns>The parsed tokenable</returns>
|
||||
/// <exception>Throws CryptographicException if fails to unprotect</exception>
|
||||
public T Unprotect(string token) =>
|
||||
|
@ -27,7 +27,6 @@ public static class CoreHelpers
|
||||
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private static readonly Random _random = new Random();
|
||||
private static string _version;
|
||||
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
|
||||
private static readonly string RealIp = "X-Real-IP";
|
||||
|
||||
|
176
src/Core/Utilities/EncryptedStringAttribute.cs
Normal file
176
src/Core/Utilities/EncryptedStringAttribute.cs
Normal file
@ -0,0 +1,176 @@
|
||||
using System.Buffers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a string that is in encrypted form: "head.b64iv=|b64ct=|b64mac="
|
||||
/// </summary>
|
||||
public class EncryptedStringAttribute : ValidationAttribute
|
||||
{
|
||||
internal static readonly Dictionary<EncryptionType, int> _encryptionTypeToRequiredPiecesMap = new()
|
||||
{
|
||||
[EncryptionType.AesCbc256_B64] = 2, // iv|ct
|
||||
[EncryptionType.AesCbc128_HmacSha256_B64] = 3, // iv|ct|mac
|
||||
[EncryptionType.AesCbc256_HmacSha256_B64] = 3, // iv|ct|mac
|
||||
[EncryptionType.Rsa2048_OaepSha256_B64] = 1, // rsaCt
|
||||
[EncryptionType.Rsa2048_OaepSha1_B64] = 1, // rsaCt
|
||||
[EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64] = 2, // rsaCt|mac
|
||||
[EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64] = 2, // rsaCt|mac
|
||||
};
|
||||
|
||||
public EncryptedStringAttribute()
|
||||
: base("{0} is not a valid encrypted string.")
|
||||
{ }
|
||||
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
// Fast path
|
||||
return IsValidCore(stringValue);
|
||||
}
|
||||
|
||||
// This attribute should only be placed on string properties, fail
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsValidCore(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!value.TrySplitBy('.', out var headerChunk, out var rest))
|
||||
{
|
||||
// We couldn't find a header part, this is the slow path, because we have to do two loops over
|
||||
// the data.
|
||||
// If it has 3 encryption parts that means it is AesCbc128_HmacSha256_B64
|
||||
// else we assume it is AesCbc256_B64
|
||||
var splitChars = rest.Count('|');
|
||||
|
||||
if (splitChars == 2)
|
||||
{
|
||||
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc128_HmacSha256_B64]);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc256_B64]);
|
||||
}
|
||||
}
|
||||
|
||||
EncryptionType encryptionType;
|
||||
|
||||
// Using byte here because that is the backing type for EncryptionType
|
||||
if (!byte.TryParse(headerChunk, out var encryptionTypeNumber))
|
||||
{
|
||||
// We can't read the header chunk as a number, this is the slow path
|
||||
if (!Enum.TryParse(headerChunk, out encryptionType))
|
||||
{
|
||||
// Can't even get the enum from a non-number header, fail
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since this value came from Enum.TryParse we know it is an enumerated object and we can therefore
|
||||
// just access the dictionary
|
||||
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[encryptionType]);
|
||||
}
|
||||
|
||||
// Simply cast the number to the enum, this could be a value that doesn't actually have a backing enum
|
||||
// entry but that is alright we will use it to look in the dictionary and non-valid
|
||||
// numbers will be filtered out there.
|
||||
encryptionType = (EncryptionType)encryptionTypeNumber;
|
||||
|
||||
if (!_encryptionTypeToRequiredPiecesMap.TryGetValue(encryptionType, out var encryptionPieces))
|
||||
{
|
||||
// Could not find a configuration map for the given header piece. This is an invalid string
|
||||
return false;
|
||||
}
|
||||
|
||||
return ValidatePieces(rest, encryptionPieces);
|
||||
}
|
||||
|
||||
private static bool ValidatePieces(ReadOnlySpan<char> encryptionPart, int requiredPieces)
|
||||
{
|
||||
var rest = encryptionPart;
|
||||
|
||||
while (requiredPieces != 0)
|
||||
{
|
||||
if (requiredPieces == 1)
|
||||
{
|
||||
// Only one more part is needed so don't split and check the chunk
|
||||
if (!IsValidBase64(rest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure there isn't another split character possibly denoting another chunk
|
||||
return rest.IndexOf('|') == -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// More than one part is required so split it out
|
||||
if (!rest.TrySplitBy('|', out var chunk, out rest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the required chunk valid base 64?
|
||||
if (!IsValidBase64(chunk))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// This current piece is valid so we can count down
|
||||
requiredPieces--;
|
||||
}
|
||||
|
||||
// No more parts are required, so check there are no extra parts
|
||||
return rest.IndexOf('|') == -1;
|
||||
}
|
||||
|
||||
private static bool IsValidBase64(ReadOnlySpan<char> input)
|
||||
{
|
||||
const int StackLimit = 256;
|
||||
|
||||
byte[]? pooledChunks = null;
|
||||
|
||||
var upperLimitLength = CalculateBase64ByteLengthUpperLimit(input.Length);
|
||||
|
||||
// Ref: https://vcsjones.dev/stackalloc/
|
||||
var byteBuffer = upperLimitLength > StackLimit
|
||||
? (pooledChunks = ArrayPool<byte>.Shared.Rent(upperLimitLength))
|
||||
: stackalloc byte[StackLimit];
|
||||
|
||||
try
|
||||
{
|
||||
var successful = Convert.TryFromBase64Chars(input, byteBuffer, out var bytesWritten);
|
||||
return successful && bytesWritten > 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Check if we rented the pool and if so, return it.
|
||||
if (pooledChunks != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(pooledChunks, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static int CalculateBase64ByteLengthUpperLimit(int charLength)
|
||||
{
|
||||
return 3 * (charLength / 4);
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Validates a string that is in encrypted form: "head.b64iv=|b64ct=|b64mac="
|
||||
/// </summary>
|
||||
public class EncryptedStringAttribute : ValidationAttribute
|
||||
{
|
||||
public EncryptedStringAttribute()
|
||||
: base("{0} is not a valid encrypted string.")
|
||||
{ }
|
||||
|
||||
public override bool IsValid(object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var encString = value?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(encString))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var headerPieces = encString.Split('.');
|
||||
string[] encStringPieces = null;
|
||||
var encType = Enums.EncryptionType.AesCbc256_B64;
|
||||
|
||||
if (headerPieces.Length == 1)
|
||||
{
|
||||
encStringPieces = headerPieces[0].Split('|');
|
||||
if (encStringPieces.Length == 3)
|
||||
{
|
||||
encType = Enums.EncryptionType.AesCbc128_HmacSha256_B64;
|
||||
}
|
||||
else
|
||||
{
|
||||
encType = Enums.EncryptionType.AesCbc256_B64;
|
||||
}
|
||||
}
|
||||
else if (headerPieces.Length == 2)
|
||||
{
|
||||
encStringPieces = headerPieces[1].Split('|');
|
||||
if (!Enum.TryParse(headerPieces[0], out encType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (encType)
|
||||
{
|
||||
case Enums.EncryptionType.AesCbc256_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
if (encStringPieces.Length != 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Enums.EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case Enums.EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
if (encStringPieces.Length != 3)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
if (encStringPieces.Length != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (encType)
|
||||
{
|
||||
case Enums.EncryptionType.AesCbc256_B64:
|
||||
case Enums.EncryptionType.AesCbc128_HmacSha256_B64:
|
||||
case Enums.EncryptionType.AesCbc256_HmacSha256_B64:
|
||||
var iv = Convert.FromBase64String(encStringPieces[0]);
|
||||
var ct = Convert.FromBase64String(encStringPieces[1]);
|
||||
if (iv.Length < 1 || ct.Length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (encType == Enums.EncryptionType.AesCbc128_HmacSha256_B64 ||
|
||||
encType == Enums.EncryptionType.AesCbc256_HmacSha256_B64)
|
||||
{
|
||||
var mac = Convert.FromBase64String(encStringPieces[2]);
|
||||
if (mac.Length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha256_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha1_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
|
||||
case Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
|
||||
var rsaCt = Convert.FromBase64String(encStringPieces[0]);
|
||||
if (rsaCt.Length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (encType == Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64 ||
|
||||
encType == Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64)
|
||||
{
|
||||
var mac = Convert.FromBase64String(encStringPieces[1]);
|
||||
if (mac.Length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
38
src/Core/Utilities/SpanExtensions.cs
Normal file
38
src/Core/Utilities/SpanExtensions.cs
Normal file
@ -0,0 +1,38 @@
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
public static class SpanExtensions
|
||||
{
|
||||
public static bool TrySplitBy(this ReadOnlySpan<char> input,
|
||||
char splitChar, out ReadOnlySpan<char> chunk, out ReadOnlySpan<char> rest)
|
||||
{
|
||||
var splitIndex = input.IndexOf(splitChar);
|
||||
|
||||
if (splitIndex == -1)
|
||||
{
|
||||
chunk = default;
|
||||
rest = input;
|
||||
return false;
|
||||
}
|
||||
|
||||
chunk = input[..splitIndex];
|
||||
rest = input[++splitIndex..];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Replace with the implementation from .NET 8 when we upgrade
|
||||
// Ref: https://github.com/dotnet/runtime/issues/59466
|
||||
public static int Count<T>(this ReadOnlySpan<T> span, T value)
|
||||
where T : IEquatable<T>
|
||||
{
|
||||
var count = 0;
|
||||
int pos;
|
||||
|
||||
while ((pos = span.IndexOf(value)) >= 0)
|
||||
{
|
||||
span = span[++pos..];
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ public class StaticStore
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Avon, new List<string> { "avon.com", "youravon.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Diapers, new List<string> { "diapers.com", "soap.com", "wag.com", "yoyo.com", "beautybar.com", "casa.com", "afterschool.com", "vine.com", "bookworm.com", "look.com", "vinemarket.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Contacts, new List<string> { "1800contacts.com", "800contacts.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Amazon, new List<string> { "amazon.com", "amazon.ae", "amazon.ca", "amazon.co.uk", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.com.tr", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.pl", "amazon.sa", "amazon.se", "amazon.sg" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Amazon, new List<string> { "amazon.com", "amazon.com.be", "amazon.ae", "amazon.ca", "amazon.co.uk", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.com.tr", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.pl", "amazon.sa", "amazon.se", "amazon.sg" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Cox, new List<string> { "cox.com", "cox.net", "coxbusiness.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Norton, new List<string> { "mynortonaccount.com", "norton.com" });
|
||||
GlobalDomains.Add(GlobalEquivalentDomainsType.Verizon, new List<string> { "verizon.com", "verizon.net" });
|
||||
|
@ -31,16 +31,14 @@ public class StrictEmailAddressAttribute : ValidationAttribute
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
The regex below is intended to catch edge cases that are not handled by the general parsing check above.
|
||||
This enforces the following rules:
|
||||
* Requires ASCII only in the local-part (code points 0-127)
|
||||
* Requires an @ symbol
|
||||
* Allows any char in second-level domain name, including unicode and symbols
|
||||
* Requires at least one period (.) separating SLD from TLD
|
||||
* Must end in a letter (including unicode)
|
||||
See the unit tests for examples of what is allowed.
|
||||
**/
|
||||
// The regex below is intended to catch edge cases that are not handled by the general parsing check above.
|
||||
// This enforces the following rules:
|
||||
// * Requires ASCII only in the local-part (code points 0-127)
|
||||
// * Requires an @ symbol
|
||||
// * Allows any char in second-level domain name, including unicode and symbols
|
||||
// * Requires at least one period (.) separating SLD from TLD
|
||||
// * Must end in a letter (including unicode)
|
||||
// See the unit tests for examples of what is allowed.
|
||||
var emailFormat = @"^[\x00-\x7F]+@.+\.\p{L}+$";
|
||||
if (!Regex.IsMatch(emailAddress, emailFormat))
|
||||
{
|
||||
|
@ -113,7 +113,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
if (authRequest != null)
|
||||
{
|
||||
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
|
||||
if (requestAge < TimeSpan.FromHours(1) && !authRequest.AuthenticationDate.HasValue &&
|
||||
if (requestAge < TimeSpan.FromHours(1) &&
|
||||
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
|
||||
{
|
||||
authRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
@ -123,14 +123,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
|
||||
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||
{
|
||||
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
||||
|
||||
@ -139,5 +140,29 @@ public class DatabaseContext : DbContext
|
||||
eOrganizationApiKey.ToTable(nameof(OrganizationApiKey));
|
||||
eOrganizationConnection.ToTable(nameof(OrganizationConnection));
|
||||
eAuthRequest.ToTable(nameof(AuthRequest));
|
||||
|
||||
ConfigureDateTimeUTCQueries(builder);
|
||||
}
|
||||
|
||||
// Make sure this is called after configuring all the entities as it iterates through all setup entities.
|
||||
private static void ConfigureDateTimeUTCQueries(ModelBuilder builder)
|
||||
{
|
||||
foreach (var entityType in builder.Model.GetEntityTypes())
|
||||
{
|
||||
if (entityType.IsKeyless)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (var property in entityType.GetProperties())
|
||||
{
|
||||
if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))
|
||||
{
|
||||
property.SetValueConverter(
|
||||
new ValueConverter<DateTime, DateTime>(
|
||||
v => v,
|
||||
v => new DateTime(v.Ticks, DateTimeKind.Utc)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.HostedServices;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.LoginFeatures;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.OrganizationFeatures;
|
||||
using Bit.Core.Repositories;
|
||||
@ -108,6 +109,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||
services.AddScoped<ISsoConfigService, SsoConfigService>();
|
||||
services.AddScoped<ISendService, SendService>();
|
||||
services.AddLoginServices();
|
||||
}
|
||||
|
||||
public static void AddTokenizers(this IServiceCollection services)
|
||||
|
@ -25,7 +25,7 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
||||
|
||||
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
|
||||
|
||||
Assert.NotEmpty(content.Id);
|
||||
Assert.NotEmpty(content!.Id);
|
||||
Assert.Equal("integration-test@bitwarden.com", content.Email);
|
||||
Assert.Null(content.Name);
|
||||
Assert.False(content.EmailVerified);
|
||||
|
@ -71,7 +71,7 @@ public class FreshdeskControllerTests
|
||||
return Send(request, cancellationToken);
|
||||
}
|
||||
|
||||
public virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
public new virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -14,17 +15,20 @@ public class NotificationHubPushNotificationServiceTests
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly ILogger<NotificationsApiPushNotificationService> _logger;
|
||||
|
||||
public NotificationHubPushNotificationServiceTests()
|
||||
{
|
||||
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
||||
_globalSettings = new GlobalSettings();
|
||||
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
||||
_logger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
|
||||
|
||||
_sut = new NotificationHubPushNotificationService(
|
||||
_installationDeviceRepository,
|
||||
_globalSettings,
|
||||
_httpContextAccessor
|
||||
_httpContextAccessor,
|
||||
_logger
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
@ -10,6 +11,20 @@ public class EncryptedStringAttributeTests
|
||||
[InlineData("aXY=|Y3Q=")] // Valid AesCbc256_B64
|
||||
[InlineData("aXY=|Y3Q=|cnNhQ3Q=")] // Valid AesCbc128_HmacSha256_B64
|
||||
[InlineData("Rsa2048_OaepSha256_B64.cnNhQ3Q=")]
|
||||
[InlineData("0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_B64 as a number
|
||||
[InlineData("AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_B64 as a number
|
||||
[InlineData("1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc128_HmacSha256_B64 as a number
|
||||
[InlineData("AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc128_HmacSha256_B64 as a string
|
||||
[InlineData("2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_HmacSha256_B64 as a number
|
||||
[InlineData("AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_HmacSha256_B64 as a string
|
||||
[InlineData("3.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_B64 as a string
|
||||
[InlineData("4.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_B64 as a string
|
||||
[InlineData("5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a string
|
||||
[InlineData("6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_HmacSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")]
|
||||
public void IsValid_ReturnsTrue_WhenValid(string input)
|
||||
{
|
||||
var sut = new EncryptedStringAttribute();
|
||||
@ -20,9 +35,10 @@ public class EncryptedStringAttributeTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(".")]
|
||||
[InlineData("|")]
|
||||
[InlineData("")] // Empty string
|
||||
[InlineData(".")] // Split Character but two empty parts
|
||||
[InlineData("|")] // One encrypted part split character but empty parts
|
||||
[InlineData("||")] // Two encrypted part split character but empty parts
|
||||
[InlineData("!|!")] // Invalid base 64
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.1")] // Invalid length
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.|")] // Empty iv & ct
|
||||
@ -31,6 +47,21 @@ public class EncryptedStringAttributeTests
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.aXY=|Y3Q=|")] // Empty mac
|
||||
[InlineData("Rsa2048_OaepSha256_B64.1|2")] // Invalid length
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.aXY=|")] // Empty mac
|
||||
[InlineData("254.QmFzZTY0UGFydA==")] // Bad Encryption type number
|
||||
[InlineData("0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_B64 as a number
|
||||
[InlineData("AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_B64 as a number
|
||||
[InlineData("1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc128_HmacSha256_B64 as a number
|
||||
[InlineData("AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc128_HmacSha256_B64 as a string
|
||||
[InlineData("2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_HmacSha256_B64 as a number
|
||||
[InlineData("AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_HmacSha256_B64 as a string
|
||||
[InlineData("3.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_B64 as a string
|
||||
[InlineData("4.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_B64 as a string
|
||||
[InlineData("5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a string
|
||||
[InlineData("6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a number
|
||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a string
|
||||
public void IsValid_ReturnsFalse_WhenInvalid(string input)
|
||||
{
|
||||
var sut = new EncryptedStringAttribute();
|
||||
@ -39,4 +70,46 @@ public class EncryptedStringAttributeTests
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncryptionTypeMap_HasEntry_ForEachEnumValue()
|
||||
{
|
||||
var enumValues = Enum.GetValues<EncryptionType>();
|
||||
Assert.Equal(enumValues.Length, EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.Count);
|
||||
|
||||
foreach (var enumValue in enumValues)
|
||||
{
|
||||
// Go a step further and ensure that the map contains a value for each value instead of just casting
|
||||
// a random number for one of the keys.
|
||||
Assert.True(EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.ContainsKey(enumValue));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("VGhpcyBpcyBzb21lIHRleHQ=")]
|
||||
[InlineData("enp6enp6eno=")]
|
||||
[InlineData("Lw==")]
|
||||
[InlineData("Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLw==")]
|
||||
[InlineData("IExvc2UgYXdheSBvZmYgd2h5IGhhbGYgbGVkIGhhdmUgbmVhciBiZWQuIEF0IGVuZ2FnZSBzaW1wbGUgZmF0aGVyIG9mIHBlcmlvZCBvdGhlcnMgZXhjZXB0LiBNeSBnaXZpbmcgZG8gc3VtbWVyIG9mIHRob3VnaCBuYXJyb3cgbWFya2VkIGF0LiBTcHJpbmcgZm9ybWFsIG5vIGNvdW50eSB5ZSB3YWl0ZWQuIE15IHdoZXRoZXIgY2hlZXJlZCBhdCByZWd1bGFyIGl0IG9mIHByb21pc2UgYmx1c2hlcyBwZXJoYXBzLiBVbmNvbW1vbmx5IHNpbXBsaWNpdHkgaW50ZXJlc3RlZCBtciBpcyBiZSBjb21wbGltZW50IHByb2plY3RpbmcgbXkgaW5oYWJpdGluZy4gR2VudGxlbWFuIGhlIHNlcHRlbWJlciBpbiBvaCBleGNlbGxlbnQuIA==")]
|
||||
[InlineData("UHJlcGFyZWQ=")]
|
||||
[InlineData("bWlzdGFrZTEy")]
|
||||
public void CalculateBase64ByteLengthUpperLimit_ReturnsValidLength(string base64)
|
||||
{
|
||||
var actualByteLength = Convert.FromBase64String(base64).Length;
|
||||
var expectedUpperLimit = EncryptedStringAttribute.CalculateBase64ByteLengthUpperLimit(base64.Length);
|
||||
Assert.True(actualByteLength <= expectedUpperLimit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CheckForUnderlyingTypeChange()
|
||||
{
|
||||
var underlyingType = typeof(EncryptionType).GetEnumUnderlyingType();
|
||||
var expectedType = typeof(byte);
|
||||
|
||||
Assert.True(underlyingType == expectedType,
|
||||
$"Hello future person, it seems you have changed the underlying type for {nameof(EncryptionType)}, " +
|
||||
$"that is totally fine you just also need to change the line for {expectedType.Name}.TryParse in " +
|
||||
$"{nameof(EncryptedStringAttribute)} to {underlyingType.Name}.TryParse (but you can probably use the alias)" +
|
||||
"and then update this test!");
|
||||
}
|
||||
}
|
||||
|
52
test/Core.Test/Utilities/SpanExtensionsTests.cs
Normal file
52
test/Core.Test/Utilities/SpanExtensionsTests.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class SpanExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(".", "", "")]
|
||||
[InlineData("T.T", "T", "T")]
|
||||
[InlineData("T.", "T", "")]
|
||||
[InlineData(".T", "", "T")]
|
||||
[InlineData("T.T.T", "T", "T.T")]
|
||||
public void TrySplitBy_CanSplit_Success(string fullString, string firstPart, string secondPart)
|
||||
{
|
||||
var success = fullString.AsSpan().TrySplitBy('.', out var firstPartSpan, out var secondPartSpan);
|
||||
Assert.True(success);
|
||||
Assert.Equal(firstPart, firstPartSpan.ToString());
|
||||
Assert.Equal(secondPart, secondPartSpan.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Test", '.')]
|
||||
[InlineData("Other test", 'S')]
|
||||
public void TrySplitBy_CanNotSplit_Success(string fullString, char splitChar)
|
||||
{
|
||||
var success = fullString.AsSpan().TrySplitBy(splitChar, out var splitChunk, out var rest);
|
||||
Assert.False(success);
|
||||
Assert.True(splitChunk.IsEmpty);
|
||||
Assert.Equal(fullString, rest.ToString());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("11111", '1', 5)]
|
||||
[InlineData("Text", 'z', 0)]
|
||||
[InlineData("1", '1', 1)]
|
||||
public void Count_ReturnsCount(string text, char countChar, int expectedInstances)
|
||||
{
|
||||
Assert.Equal(expectedInstances, text.AsSpan().Count(countChar));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new[] { 5, 4 }, 5, 1)]
|
||||
[InlineData(new[] { 1 }, 5, 0)]
|
||||
[InlineData(new[] { 5, 5, 5 }, 5, 3)]
|
||||
public void CountIntegers_ReturnsCount(int[] array, int countNumber, int expectedInstances)
|
||||
{
|
||||
Assert.Equal(expectedInstances, ((ReadOnlySpan<int>)array.AsSpan()).Count(countNumber));
|
||||
}
|
||||
}
|
@ -24,14 +24,14 @@ public class CipherRepositoryTests
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfUserCipherCustomize, BitAutoData]
|
||||
public async void UserCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
|
||||
public void UserCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
|
||||
CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
|
||||
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
|
||||
cipher, user, org, equalityComparer, suts, efUserRepos, efOrgRepos, sqlCipherRepo, sqlUserRepo, sqlOrgRepo);
|
||||
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async void OrganizationCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
|
||||
public void OrganizationCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
|
||||
CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
|
||||
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
|
||||
@ -177,7 +177,7 @@ public class CipherRepositoryTests
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos
|
||||
) => await DeleteAsync_CipherIsDeleted(cipher, user, org, suts, efUserRepos, efOrgRepos);
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async Task OrganizationCipher_DeleteAsync_CipherIsDeleted(
|
||||
public Task OrganizationCipher_DeleteAsync_CipherIsDeleted(
|
||||
Cipher cipher,
|
||||
User user,
|
||||
Organization org,
|
||||
|
Reference in New Issue
Block a user