55 changed files with 1058 additions and 269 deletions
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
using Bit.Core.AdminConsole.Enums; |
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; |
||||
using Bit.Core.Enums; |
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; |
||||
|
||||
/// <summary> |
||||
/// Policy requirements for the Master Password Requirements policy. |
||||
/// </summary> |
||||
public class MasterPasswordPolicyRequirement : IPolicyRequirement |
||||
{ |
||||
/// <summary> |
||||
/// Indicates whether MasterPassword requirements are enabled for the user. |
||||
/// </summary> |
||||
public bool Enabled { get; init; } |
||||
|
||||
/// <summary> |
||||
/// Master Password Policy data model associated with this Policy |
||||
/// </summary> |
||||
public MasterPasswordPolicyData? EnforcedOptions { get; init; } |
||||
} |
||||
|
||||
public class MasterPasswordPolicyRequirementFactory : BasePolicyRequirementFactory<MasterPasswordPolicyRequirement> |
||||
{ |
||||
public override PolicyType PolicyType => PolicyType.MasterPassword; |
||||
|
||||
protected override bool ExemptProviders => false; |
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles => []; |
||||
|
||||
protected override IEnumerable<OrganizationUserStatusType> ExemptStatuses => |
||||
[OrganizationUserStatusType.Accepted, |
||||
OrganizationUserStatusType.Invited, |
||||
OrganizationUserStatusType.Revoked, |
||||
]; |
||||
|
||||
public override MasterPasswordPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) |
||||
{ |
||||
var result = policyDetails |
||||
.Select(p => p.GetDataModel<MasterPasswordPolicyData>()) |
||||
.Aggregate( |
||||
new MasterPasswordPolicyRequirement(), |
||||
(result, data) => |
||||
{ |
||||
data.CombineWith(result.EnforcedOptions); |
||||
return new MasterPasswordPolicyRequirement |
||||
{ |
||||
Enabled = true, |
||||
EnforcedOptions = data |
||||
}; |
||||
}); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Utilities; |
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Response; |
||||
|
||||
public class MasterPasswordUnlockResponseModel |
||||
{ |
||||
public required MasterPasswordUnlockKdfResponseModel Kdf { get; init; } |
||||
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; init; } |
||||
[StringLength(256)] public required string Salt { get; init; } |
||||
} |
||||
|
||||
public class MasterPasswordUnlockKdfResponseModel |
||||
{ |
||||
public required KdfType KdfType { get; init; } |
||||
public required int Iterations { get; init; } |
||||
public int? Memory { get; init; } |
||||
public int? Parallelism { get; init; } |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Response; |
||||
|
||||
public class UserDecryptionResponseModel |
||||
{ |
||||
/// <summary> |
||||
/// Returns the unlock data when the user has a master password that can be used to decrypt their vault. |
||||
/// </summary> |
||||
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; } |
||||
} |
||||
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
using Bit.Api.IntegrationTest.Factories; |
||||
using Bit.Api.IntegrationTest.Helpers; |
||||
using Bit.Api.Vault.Models.Response; |
||||
using Bit.Core.Enums; |
||||
using Bit.Core.Repositories; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Api.IntegrationTest.Vault.Controllers; |
||||
|
||||
public class SyncControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime |
||||
{ |
||||
private readonly HttpClient _client; |
||||
private readonly ApiApplicationFactory _factory; |
||||
|
||||
private readonly LoginHelper _loginHelper; |
||||
|
||||
private readonly IUserRepository _userRepository; |
||||
private string _ownerEmail = null!; |
||||
|
||||
public SyncControllerTests(ApiApplicationFactory factory) |
||||
{ |
||||
_factory = factory; |
||||
_client = factory.CreateClient(); |
||||
_loginHelper = new LoginHelper(_factory, _client); |
||||
_userRepository = _factory.GetService<IUserRepository>(); |
||||
} |
||||
|
||||
public async Task InitializeAsync() |
||||
{ |
||||
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; |
||||
await _factory.LoginWithNewAccount(_ownerEmail); |
||||
} |
||||
|
||||
public Task DisposeAsync() |
||||
{ |
||||
_client.Dispose(); |
||||
return Task.CompletedTask; |
||||
} |
||||
|
||||
[Fact] |
||||
// [BitAutoData] |
||||
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull() |
||||
{ |
||||
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; |
||||
await _factory.LoginWithNewAccount(tempEmail); |
||||
await _loginHelper.LoginAsync(tempEmail); |
||||
|
||||
// Remove user's password. |
||||
var user = await _userRepository.GetByEmailAsync(tempEmail); |
||||
Assert.NotNull(user); |
||||
user.MasterPassword = null; |
||||
await _userRepository.UpsertAsync(user); |
||||
|
||||
var response = await _client.GetAsync("/sync"); |
||||
response.EnsureSuccessStatusCode(); |
||||
|
||||
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>(); |
||||
|
||||
Assert.NotNull(syncResponseModel); |
||||
Assert.NotNull(syncResponseModel.UserDecryption); |
||||
Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock); |
||||
} |
||||
|
||||
[Theory] |
||||
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)] |
||||
[BitAutoData(KdfType.Argon2id, 11, 128, 5)] |
||||
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull( |
||||
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism) |
||||
{ |
||||
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; |
||||
await _factory.LoginWithNewAccount(tempEmail); |
||||
await _loginHelper.LoginAsync(tempEmail); |
||||
|
||||
// Change KDF settings |
||||
var user = await _userRepository.GetByEmailAsync(tempEmail); |
||||
Assert.NotNull(user); |
||||
user.Kdf = kdfType; |
||||
user.KdfIterations = kdfIterations; |
||||
user.KdfMemory = kdfMemory; |
||||
user.KdfParallelism = kdfParallelism; |
||||
await _userRepository.UpsertAsync(user); |
||||
|
||||
var response = await _client.GetAsync("/sync"); |
||||
response.EnsureSuccessStatusCode(); |
||||
|
||||
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>(); |
||||
|
||||
Assert.NotNull(syncResponseModel); |
||||
Assert.NotNull(syncResponseModel.UserDecryption); |
||||
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock); |
||||
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf); |
||||
Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType); |
||||
Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations); |
||||
Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory); |
||||
Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism); |
||||
Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey); |
||||
Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt); |
||||
} |
||||
} |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
using System.Text.Json; |
||||
using Bit.Core.AdminConsole.Enums; |
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; |
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; |
||||
using Bit.Core.Test.AdminConsole.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture; |
||||
using Bit.Test.Common.AutoFixture.Attributes; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; |
||||
|
||||
[SutProviderCustomize] |
||||
public class MasterPasswordPolicyRequirementFactoryTests |
||||
{ |
||||
[Theory, BitAutoData] |
||||
public void MasterPasswordPolicyData_CombineWith_Joins_Policy_Options(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider) |
||||
{ |
||||
var mpd1 = JsonSerializer.Serialize(new MasterPasswordPolicyData { MinLength = 20, RequireLower = false, RequireSpecial = false }); |
||||
var mpd2 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireLower = true }); |
||||
var mpd3 = JsonSerializer.Serialize(new MasterPasswordPolicyData { RequireSpecial = true }); |
||||
|
||||
var policyDetails1 = new PolicyDetails |
||||
{ |
||||
PolicyType = PolicyType.MasterPassword, |
||||
PolicyData = mpd1 |
||||
}; |
||||
|
||||
var policyDetails2 = new PolicyDetails |
||||
{ |
||||
PolicyType = PolicyType.MasterPassword, |
||||
PolicyData = mpd2 |
||||
}; |
||||
var policyDetails3 = new PolicyDetails |
||||
{ |
||||
PolicyType = PolicyType.MasterPassword, |
||||
PolicyData = mpd3 |
||||
}; |
||||
|
||||
|
||||
var actual = sutProvider.Sut.Create([policyDetails1, policyDetails2, policyDetails3]); |
||||
|
||||
Assert.NotNull(actual); |
||||
Assert.True(actual.Enabled); |
||||
Assert.True(actual.EnforcedOptions.RequireLower); |
||||
Assert.True(actual.EnforcedOptions.RequireSpecial); |
||||
Assert.Equal(20, actual.EnforcedOptions.MinLength); |
||||
} |
||||
|
||||
[Theory, BitAutoData] |
||||
public void MasterPassword_IsFalse_IfNoPolicies(SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider) |
||||
{ |
||||
var actual = sutProvider.Sut.Create([]); |
||||
|
||||
Assert.False(actual.Enabled); |
||||
Assert.Null(actual.EnforcedOptions); |
||||
} |
||||
|
||||
[Theory, BitAutoData] |
||||
public void MasterPassword_IsTrue_IfAnyDisableSendPolicies( |
||||
[PolicyDetails(PolicyType.MasterPassword)] PolicyDetails[] policies, |
||||
SutProvider<MasterPasswordPolicyRequirementFactory> sutProvider) |
||||
{ |
||||
var actual = sutProvider.Sut.Create(policies); |
||||
|
||||
Assert.True(actual.Enabled); |
||||
Assert.NotNull(actual.EnforcedOptions); |
||||
Assert.NotNull(actual.EnforcedOptions.EnforceOnLogin); |
||||
Assert.NotNull(actual.EnforcedOptions.RequireLower); |
||||
Assert.NotNull(actual.EnforcedOptions.RequireNumbers); |
||||
Assert.NotNull(actual.EnforcedOptions.RequireSpecial); |
||||
Assert.NotNull(actual.EnforcedOptions.RequireUpper); |
||||
Assert.Null(actual.EnforcedOptions.MinComplexity); |
||||
Assert.Null(actual.EnforcedOptions.MinLength); |
||||
} |
||||
} |
||||
@ -0,0 +1,195 @@
@@ -0,0 +1,195 @@
|
||||
using System.Net; |
||||
using System.Net.Sockets; |
||||
using System.Text; |
||||
using Bit.Core.Utilities; |
||||
using Microsoft.AspNetCore.Hosting; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Microsoft.Extensions.Logging; |
||||
using NSubstitute; |
||||
using Serilog; |
||||
using Serilog.Extensions.Logging; |
||||
using Xunit; |
||||
|
||||
namespace Bit.Core.Test.Utilities; |
||||
|
||||
public class LoggerFactoryExtensionsTests |
||||
{ |
||||
[Fact] |
||||
public void AddSerilog_IsDevelopment_AddsNoProviders() |
||||
{ |
||||
var providers = GetProviders([], "Development"); |
||||
|
||||
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() |
||||
{ |
||||
var providers = GetProviders([]); |
||||
|
||||
var provider = Assert.Single(providers); |
||||
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task AddSerilog_FileLogging_Old_Works() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory(); |
||||
|
||||
var providers = GetProviders(new Dictionary<string, string?> |
||||
{ |
||||
{ "GlobalSettings:ProjectName", "Test" }, |
||||
{ "GlobalSetting:LogDirectoryByProject", "true" }, |
||||
{ "GlobalSettings:LogDirectory", tempDir.FullName }, |
||||
}); |
||||
|
||||
var provider = Assert.Single(providers); |
||||
Assert.IsAssignableFrom<SerilogLoggerProvider>(provider); |
||||
|
||||
var logger = provider.CreateLogger("Test"); |
||||
logger.LogWarning("This is a test"); |
||||
|
||||
var logFile = Assert.Single(tempDir.EnumerateFiles("Test/*.txt")); |
||||
|
||||
var logFileContents = await File.ReadAllTextAsync(logFile.FullName); |
||||
|
||||
Assert.Contains( |
||||
"This is a test", |
||||
logFileContents |
||||
); |
||||
} |
||||
|
||||
[Fact] |
||||
public async Task AddSerilog_FileLogging_New_Works() |
||||
{ |
||||
var tempDir = Directory.CreateTempSubdirectory(); |
||||
|
||||
var provider = GetServiceProvider(new Dictionary<string, string?> |
||||
{ |
||||
{ "Logging:PathFormat", $"{tempDir}/Logs/log-{{Date}}.log" }, |
||||
}, "Production"); |
||||
|
||||
var logger = provider |
||||
.GetRequiredService<ILoggerFactory>() |
||||
.CreateLogger("Test"); |
||||
|
||||
logger.LogWarning("This is a test"); |
||||
|
||||
// Writing to the file is buffered, give it a little time to flush |
||||
await Task.Delay(5); |
||||
|
||||
var logFile = Assert.Single(tempDir.EnumerateFiles("Logs/*.log")); |
||||
|
||||
var logFileContents = await File.ReadAllTextAsync(logFile.FullName); |
||||
|
||||
Assert.DoesNotContain( |
||||
"This configuration location for file logging has been deprecated.", |
||||
logFileContents |
||||
); |
||||
Assert.Contains( |
||||
"This is a test", |
||||
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); |
||||
return provider.GetServices<ILoggerProvider>(); |
||||
} |
||||
|
||||
private static IServiceProvider GetServiceProvider(Dictionary<string, string?> initialData, string environment) |
||||
{ |
||||
var config = new ConfigurationBuilder() |
||||
.AddInMemoryCollection(initialData) |
||||
.Build(); |
||||
|
||||
var hostingEnvironment = Substitute.For<IWebHostEnvironment>(); |
||||
|
||||
hostingEnvironment |
||||
.EnvironmentName |
||||
.Returns(environment); |
||||
|
||||
var context = new WebHostBuilderContext |
||||
{ |
||||
HostingEnvironment = hostingEnvironment, |
||||
Configuration = config, |
||||
}; |
||||
|
||||
var services = new ServiceCollection(); |
||||
services.AddLogging(builder => |
||||
{ |
||||
builder.AddSerilog(context); |
||||
}); |
||||
|
||||
return services.BuildServiceProvider(); |
||||
} |
||||
} |
||||
@ -1,40 +0,0 @@
@@ -1,40 +0,0 @@
|
||||
FROM nginx:stable |
||||
|
||||
LABEL com.bitwarden.product="bitwarden" |
||||
|
||||
ENV USERNAME="bitwarden" |
||||
ENV GROUPNAME="bitwarden" |
||||
|
||||
RUN apt-get update && \ |
||||
apt-get install -y --no-install-recommends \ |
||||
gosu \ |
||||
curl && \ |
||||
rm -rf /var/lib/apt/lists/* |
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf |
||||
COPY proxy.conf /etc/nginx/proxy.conf |
||||
COPY mime.types /etc/nginx/mime.types |
||||
COPY security-headers.conf /etc/nginx/security-headers.conf |
||||
COPY security-headers-ssl.conf /etc/nginx/security-headers.conf |
||||
|
||||
COPY setup-bwuser.sh / |
||||
|
||||
EXPOSE 8000 |
||||
|
||||
EXPOSE 8080 |
||||
EXPOSE 8443 |
||||
|
||||
RUN chmod +x /setup-bwuser.sh |
||||
|
||||
RUN ./setup-bwuser.sh $USERNAME $GROUPNAME |
||||
|
||||
RUN mkdir -p /var/run/nginx && \ |
||||
touch /var/run/nginx/nginx.pid |
||||
RUN chown -R $USERNAME:$GROUPNAME /var/run/nginx && \ |
||||
chown -R $USERNAME:$GROUPNAME /var/cache/nginx && \ |
||||
chown -R $USERNAME:$GROUPNAME /var/log/nginx |
||||
|
||||
|
||||
HEALTHCHECK CMD curl --insecure -Lfs https://localhost:8443/alive || curl -Lfs http://localhost:8080/alive || exit 1 |
||||
|
||||
USER bitwarden |
||||
Loading…
Reference in new issue