mirror of
https://github.com/bitwarden/server
synced 2026-01-02 00:23:40 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using Bit.Core.Auth.Attributes;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Attributes;
|
||||
|
||||
public class MarketingInitiativeValidationAttributeTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsValid_NullValue_ReturnsTrue()
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(null);
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MarketingInitiativeConstants.Premium)]
|
||||
public void IsValid_AcceptedValue_ReturnsTrue(string value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("")]
|
||||
[InlineData("Premium")] // case sensitive - capitalized
|
||||
[InlineData("PREMIUM")] // case sensitive - uppercase
|
||||
[InlineData("premium ")] // trailing space
|
||||
[InlineData(" premium")] // leading space
|
||||
public void IsValid_InvalidStringValue_ReturnsFalse(string value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(123)] // integer
|
||||
[InlineData(true)] // boolean
|
||||
[InlineData(45.67)] // double
|
||||
public void IsValid_NonStringValue_ReturnsFalse(object value)
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var actual = sut.IsValid(value);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ErrorMessage_ContainsAcceptedValues()
|
||||
{
|
||||
var sut = new MarketingInitiativeValidationAttribute();
|
||||
|
||||
var errorMessage = sut.ErrorMessage;
|
||||
|
||||
Assert.NotNull(errorMessage);
|
||||
Assert.Contains("premium", errorMessage);
|
||||
Assert.Contains("Marketing initiative type must be one of:", errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot tests to ensure the string constants in <see cref="MarketingInitiativeConstants"/> do not change unintentionally.
|
||||
/// If you intentionally change any of these values, please update the tests to reflect the new expected values.
|
||||
/// </summary>
|
||||
public class MarketingInitiativeConstantsSnapshotTests
|
||||
{
|
||||
[Fact]
|
||||
public void MarketingInitiativeConstants_HaveCorrectValues()
|
||||
{
|
||||
// Assert
|
||||
Assert.Equal("premium", MarketingInitiativeConstants.Premium);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Serilog;
|
||||
using Serilog.Extensions.Logging;
|
||||
using Xunit;
|
||||
|
||||
@@ -23,18 +19,6 @@ public class LoggerFactoryExtensionsTests
|
||||
Assert.Empty(providers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSerilog_IsDevelopment_DevLoggingEnabled_AddsSerilog()
|
||||
{
|
||||
var providers = GetProviders(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:EnableDevLogging", "true" },
|
||||
}, "Development");
|
||||
|
||||
var provider = Assert.Single(providers);
|
||||
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddSerilog_IsProduction_AddsSerilog()
|
||||
{
|
||||
@@ -52,7 +36,7 @@ public class LoggerFactoryExtensionsTests
|
||||
var providers = GetProviders(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:ProjectName", "Test" },
|
||||
{ "GlobalSetting:LogDirectoryByProject", "true" },
|
||||
{ "GlobalSettings:LogDirectoryByProject", "true" },
|
||||
{ "GlobalSettings:LogDirectory", tempDir.FullName },
|
||||
});
|
||||
|
||||
@@ -62,6 +46,8 @@ public class LoggerFactoryExtensionsTests
|
||||
var logger = provider.CreateLogger("Test");
|
||||
logger.LogWarning("This is a test");
|
||||
|
||||
provider.Dispose();
|
||||
|
||||
var logFile = Assert.Single(tempDir.EnumerateFiles("Test/*.txt"));
|
||||
|
||||
var logFileContents = await File.ReadAllTextAsync(logFile.FullName);
|
||||
@@ -104,62 +90,6 @@ public class LoggerFactoryExtensionsTests
|
||||
logFileContents
|
||||
);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Only for local development.")]
|
||||
public async Task AddSerilog_SyslogConfigured_Warns()
|
||||
{
|
||||
// Setup a fake syslog server
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
using var listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 25000);
|
||||
listener.Start();
|
||||
|
||||
var provider = GetServiceProvider(new Dictionary<string, string?>
|
||||
{
|
||||
{ "GlobalSettings:SysLog:Destination", "tcp://127.0.0.1:25000" },
|
||||
{ "GlobalSettings:SiteName", "TestSite" },
|
||||
{ "GlobalSettings:ProjectName", "TestProject" },
|
||||
}, "Production");
|
||||
|
||||
var loggerFactory = provider.GetRequiredService<ILoggerFactory>();
|
||||
var logger = loggerFactory.CreateLogger("Test");
|
||||
|
||||
logger.LogWarning("This is a test");
|
||||
|
||||
// Look in syslog for data
|
||||
using var socket = await listener.AcceptSocketAsync(cts.Token);
|
||||
|
||||
// This is rather lazy as opposed to implementing smarter syslog message
|
||||
// reading but thats not what this test about, so instead just give
|
||||
// the sink time to finish its work in the background
|
||||
|
||||
List<string> messages = [];
|
||||
|
||||
while (true)
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var received = await socket.ReceiveAsync(buffer, SocketFlags.None, cts.Token);
|
||||
|
||||
if (received == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var response = Encoding.ASCII.GetString(buffer, 0, received);
|
||||
messages.Add(response);
|
||||
|
||||
if (messages.Count == 2)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Collection(
|
||||
messages,
|
||||
(firstMessage) => Assert.Contains("Syslog for logging has been deprecated", firstMessage),
|
||||
(secondMessage) => Assert.Contains("This is a test", secondMessage)
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<ILoggerProvider> GetProviders(Dictionary<string, string?> initialData, string environment = "Production")
|
||||
{
|
||||
var provider = GetServiceProvider(initialData, environment);
|
||||
@@ -172,23 +102,34 @@ public class LoggerFactoryExtensionsTests
|
||||
.AddInMemoryCollection(initialData)
|
||||
.Build();
|
||||
|
||||
var hostingEnvironment = Substitute.For<IWebHostEnvironment>();
|
||||
var hostingEnvironment = Substitute.For<IHostEnvironment>();
|
||||
|
||||
hostingEnvironment
|
||||
.EnvironmentName
|
||||
.Returns(environment);
|
||||
|
||||
var context = new WebHostBuilderContext
|
||||
var context = new HostBuilderContext(new Dictionary<object, object>())
|
||||
{
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
Configuration = config,
|
||||
};
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.AddSerilog(context);
|
||||
});
|
||||
|
||||
var hostBuilder = Substitute.For<IHostBuilder>();
|
||||
hostBuilder
|
||||
.When(h => h.ConfigureServices(Arg.Any<Action<HostBuilderContext, IServiceCollection>>()))
|
||||
.Do(call =>
|
||||
{
|
||||
var configureAction = call.Arg<Action<HostBuilderContext, IServiceCollection>>();
|
||||
configureAction(context, services);
|
||||
});
|
||||
|
||||
hostBuilder.AddSerilogFileLogging();
|
||||
|
||||
hostBuilder
|
||||
.ConfigureServices(Arg.Any<Action<HostBuilderContext, IServiceCollection>>())
|
||||
.Received(1);
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class AccountsControllerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
|
||||
public async Task PostPasswordPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
|
||||
{
|
||||
var userKdfInfo = new UserKdfInformation
|
||||
{
|
||||
@@ -84,30 +84,113 @@ public class AccountsControllerTests : IDisposable
|
||||
};
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo);
|
||||
|
||||
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
|
||||
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" });
|
||||
|
||||
Assert.Equal(userKdfInfo.Kdf, response.Kdf);
|
||||
Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
|
||||
public async Task PostPrelogin_And_PostPasswordPrelogin_ShouldUseSamePreloginLogic()
|
||||
{
|
||||
// Arrange: No user exists and no default HMAC key to force default path
|
||||
var email = "same-user@example.com";
|
||||
SetDefaultKdfHmacKey(null);
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||
|
||||
// Act
|
||||
var legacyResponse = await _sut.PostPrelogin(new PasswordPreloginRequestModel { Email = email });
|
||||
var newResponse = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
|
||||
|
||||
// Assert: Both endpoints yield identical results, implying shared logic path
|
||||
Assert.Equal(legacyResponse.Kdf, newResponse.Kdf);
|
||||
Assert.Equal(legacyResponse.KdfIterations, newResponse.KdfIterations);
|
||||
Assert.Equal(legacyResponse.KdfMemory, newResponse.KdfMemory);
|
||||
Assert.Equal(legacyResponse.KdfParallelism, newResponse.KdfParallelism);
|
||||
Assert.Equal(legacyResponse.Salt, newResponse.Salt);
|
||||
Assert.NotNull(legacyResponse.KdfSettings);
|
||||
Assert.NotNull(newResponse.KdfSettings);
|
||||
Assert.Equal(legacyResponse.KdfSettings!.KdfType, newResponse.KdfSettings!.KdfType);
|
||||
Assert.Equal(legacyResponse.KdfSettings!.Iterations, newResponse.KdfSettings!.Iterations);
|
||||
Assert.Equal(legacyResponse.KdfSettings!.Memory, newResponse.KdfSettings!.Memory);
|
||||
Assert.Equal(legacyResponse.KdfSettings!.Parallelism, newResponse.KdfSettings!.Parallelism);
|
||||
|
||||
// Both methods should consult the repository once each with the same email
|
||||
await _userRepository.Received(2).GetKdfInformationByEmailAsync(Arg.Is<string>(e => e == email));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostPasswordPrelogin_WhenUserExists_ReturnsNewFieldsAlignedWithLegacy_Argon2()
|
||||
{
|
||||
var email = "user@example.com";
|
||||
var userKdfInfo = new UserKdfInformation
|
||||
{
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default
|
||||
};
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo);
|
||||
|
||||
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
|
||||
|
||||
// New fields exist and match repository values
|
||||
Assert.NotNull(response.KdfSettings);
|
||||
Assert.Equal(userKdfInfo.Kdf, response.KdfSettings!.KdfType);
|
||||
Assert.Equal(userKdfInfo.KdfIterations, response.KdfSettings!.Iterations);
|
||||
Assert.Equal(userKdfInfo.KdfMemory, response.KdfSettings!.Memory);
|
||||
Assert.Equal(userKdfInfo.KdfParallelism, response.KdfSettings!.Parallelism);
|
||||
|
||||
// New and legacy fields are aligned during migration
|
||||
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
|
||||
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
|
||||
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
|
||||
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
|
||||
|
||||
// Salt is set to the input email during migration
|
||||
Assert.Equal(email, response.Salt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndNoDefaultKdfHmacKeySet_ShouldDefaultToPBKDF()
|
||||
{
|
||||
SetDefaultKdfHmacKey(null);
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||
|
||||
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
|
||||
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = "user@example.com" });
|
||||
|
||||
Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf);
|
||||
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, response.KdfIterations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostPasswordPrelogin_NoUser_NoDefaultHmacKey_ReturnsAlignedNewFieldsAndSalt()
|
||||
{
|
||||
var email = "user@example.com";
|
||||
SetDefaultKdfHmacKey(null);
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||
|
||||
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
|
||||
|
||||
// New fields exist
|
||||
Assert.NotNull(response.KdfSettings);
|
||||
|
||||
// New and legacy fields are aligned during migration
|
||||
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
|
||||
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
|
||||
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
|
||||
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
|
||||
|
||||
// Salt is set to the input email during migration
|
||||
Assert.Equal(email, response.Salt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PostPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email)
|
||||
public async Task PostPasswordPrelogin_WhenUserDoesNotExistAndDefaultKdfHmacKeyIsSet_ShouldComputeHmacAndReturnExpectedKdf(string email)
|
||||
{
|
||||
// Arrange:
|
||||
var defaultKey = Encoding.UTF8.GetBytes("my-secret-key");
|
||||
var defaultKey = "my-secret-key"u8.ToArray();
|
||||
SetDefaultKdfHmacKey(defaultKey);
|
||||
|
||||
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
|
||||
@@ -122,7 +205,7 @@ public class AccountsControllerTests : IDisposable
|
||||
var expectedKdf = defaultKdfResults[expectedIndex];
|
||||
|
||||
// Act
|
||||
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = email });
|
||||
var response = await _sut.PostPasswordPrelogin(new PasswordPreloginRequestModel { Email = email });
|
||||
|
||||
// Assert: Ensure the returned KDF matches the expected one from the computed hash
|
||||
Assert.Equal(expectedKdf.Kdf, response.Kdf);
|
||||
@@ -132,6 +215,16 @@ public class AccountsControllerTests : IDisposable
|
||||
Assert.Equal(expectedKdf.KdfMemory, response.KdfMemory);
|
||||
Assert.Equal(expectedKdf.KdfParallelism, response.KdfParallelism);
|
||||
}
|
||||
|
||||
// New and legacy fields are aligned during migration
|
||||
Assert.NotNull(response.KdfSettings);
|
||||
Assert.Equal(response.Kdf, response.KdfSettings!.KdfType);
|
||||
Assert.Equal(response.KdfIterations, response.KdfSettings!.Iterations);
|
||||
Assert.Equal(response.KdfMemory, response.KdfSettings!.Memory);
|
||||
Assert.Equal(response.KdfParallelism, response.KdfSettings!.Parallelism);
|
||||
|
||||
// Salt is set to the input email during migration
|
||||
Assert.Equal(email, response.Salt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
Reference in New Issue
Block a user