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:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -36,6 +36,7 @@ util/Setup/** @bitwarden/dept-bre @bitwarden/team-platform-dev
|
||||
|
||||
# UIF
|
||||
src/Core/MailTemplates/Mjml @bitwarden/team-ui-foundation # Teams are expected to own sub-directories of this project
|
||||
src/Core/MailTemplates/Mjml/.mjmlconfig # This change allows teams to add components within their own subdirectories without requiring a code review from UIF.
|
||||
|
||||
# Auth team
|
||||
**/Auth @bitwarden/team-auth-dev
|
||||
|
||||
4
.github/renovate.json5
vendored
4
.github/renovate.json5
vendored
@@ -90,11 +90,7 @@
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Newtonsoft.Json",
|
||||
"NSubstitute",
|
||||
"Sentry.Serilog",
|
||||
"Serilog.AspNetCore",
|
||||
"Serilog.Extensions.Logging",
|
||||
"Serilog.Extensions.Logging.File",
|
||||
"Serilog.Sinks.SyslogMessages",
|
||||
"Stripe.net",
|
||||
"Swashbuckle.AspNetCore",
|
||||
"Swashbuckle.AspNetCore.SwaggerGen",
|
||||
|
||||
@@ -11,21 +11,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.ScimSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -94,11 +94,8 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog;
|
||||
|
||||
namespace Bit.Sso;
|
||||
|
||||
@@ -13,19 +12,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return e.Level >= globalSettings.MinLogLevel.SsoSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -100,8 +100,6 @@ public class Startup
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
}
|
||||
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -16,19 +16,8 @@ public class Program
|
||||
o.Limits.MaxRequestLineSize = 20_000;
|
||||
});
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return e.Level >= globalSettings.MinLogLevel.AdminSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -132,11 +132,8 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api;
|
||||
|
||||
@@ -17,32 +12,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (e.Exception != null &&
|
||||
(e.Exception.GetType() == typeof(SecurityTokenValidationException) ||
|
||||
e.Exception.Message == "Bad security stamp."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
context.Contains(typeof(IpRateLimitMiddleware).FullName))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IpRateLimit;
|
||||
}
|
||||
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.IdentityToken;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.ApiSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -234,12 +234,10 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<Startup> logger)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
"send": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
||||
@@ -11,25 +11,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.StartsWith("\"Bit.Billing.Jobs") || context.StartsWith("\"Bit.Core.Jobs"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Jobs;
|
||||
}
|
||||
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.BillingSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories.Noop;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
@@ -129,12 +128,8 @@ public class Startup
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
IWebHostEnvironment env)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
namespace Bit.Core.Auth.Attributes;
|
||||
|
||||
public class MarketingInitiativeValidationAttribute : ValidationAttribute
|
||||
{
|
||||
private static readonly string[] _acceptedValues = [MarketingInitiativeConstants.Premium];
|
||||
|
||||
public MarketingInitiativeValidationAttribute()
|
||||
{
|
||||
ErrorMessage = $"Marketing initiative type must be one of: {string.Join(", ", _acceptedValues)}";
|
||||
}
|
||||
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is not string str)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _acceptedValues.Contains(str);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
public static class MarketingInitiativeConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the user began the registration process on a marketing page designed
|
||||
/// to streamline users who intend to setup a premium subscription after registration.
|
||||
/// </summary>
|
||||
public const string Premium = "premium";
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Auth.Attributes;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
@@ -11,4 +12,6 @@ public class RegisterSendVerificationEmailRequestModel
|
||||
[StringLength(256)]
|
||||
public required string Email { get; set; }
|
||||
public bool ReceiveMarketingEmails { get; set; }
|
||||
[MarketingInitiativeValidation]
|
||||
public string? FromMarketing { get; set; }
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
||||
new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.PlanType), ((int)entity.PlanType).ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()),
|
||||
|
||||
@@ -399,7 +399,6 @@ public class OrganizationLicense : ILicense
|
||||
var installationId = claimsPrincipal.GetValue<Guid>(nameof(InstallationId));
|
||||
var licenseKey = claimsPrincipal.GetValue<string>(nameof(LicenseKey));
|
||||
var enabled = claimsPrincipal.GetValue<bool>(nameof(Enabled));
|
||||
var planType = claimsPrincipal.GetValue<PlanType>(nameof(PlanType));
|
||||
var seats = claimsPrincipal.GetValue<int?>(nameof(Seats));
|
||||
var maxCollections = claimsPrincipal.GetValue<short?>(nameof(MaxCollections));
|
||||
var useGroups = claimsPrincipal.GetValue<bool>(nameof(UseGroups));
|
||||
@@ -425,12 +424,18 @@ public class OrganizationLicense : ILicense
|
||||
var useOrganizationDomains = claimsPrincipal.GetValue<bool>(nameof(UseOrganizationDomains));
|
||||
var useAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(nameof(UseAutomaticUserConfirmation));
|
||||
|
||||
var claimedPlanType = claimsPrincipal.GetValue<PlanType>(nameof(PlanType));
|
||||
|
||||
var planTypesMatch = claimedPlanType == PlanType.FamiliesAnnually
|
||||
? organization.PlanType is PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2025
|
||||
: organization.PlanType == claimedPlanType;
|
||||
|
||||
return issued <= DateTime.UtcNow &&
|
||||
expires >= DateTime.UtcNow &&
|
||||
installationId == globalSettings.Installation.Id &&
|
||||
licenseKey == organization.LicenseKey &&
|
||||
enabled == organization.Enabled &&
|
||||
planType == organization.PlanType &&
|
||||
planTypesMatch &&
|
||||
seats == organization.Seats &&
|
||||
maxCollections == organization.MaxCollections &&
|
||||
useGroups == organization.UseGroups &&
|
||||
|
||||
@@ -163,7 +163,7 @@ public static class FeatureFlagKeys
|
||||
"pm-23174-manage-account-recovery-permission-drives-the-need-to-set-master-password";
|
||||
public const string RecoveryCodeSupportForSsoRequiredUsers = "pm-21153-recovery-code-support-for-sso-required";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
public const string MjmlWelcomeEmailTemplates = "mjml-welcome-email-templates";
|
||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
|
||||
/* Autofill Team */
|
||||
@@ -186,7 +186,6 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Billing Team */
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
|
||||
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
|
||||
public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure";
|
||||
|
||||
@@ -50,20 +50,16 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||
<PackageReference Include="OneOf" Version="3.0.271" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.2.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
<PackageReference Include="Braintree" Version="5.28.0" />
|
||||
<PackageReference Include="Stripe.net" Version="48.5.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.10.1" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.10.4" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"packages": [
|
||||
"components/mj-bw-hero",
|
||||
"components/mj-bw-icon-row",
|
||||
"components/mj-bw-learn-more-footer"
|
||||
"components/mj-bw-learn-more-footer",
|
||||
"emails/AdminConsole/components/mj-bw-inviter-info"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const { BodyComponent } = require("mjml-core");
|
||||
|
||||
class MjBwInviterInfo extends BodyComponent {
|
||||
|
||||
static dependencies = {
|
||||
"mj-column": ["mj-bw-inviter-info"],
|
||||
"mj-wrapper": ["mj-bw-inviter-info"],
|
||||
"mj-bw-inviter-info": [],
|
||||
};
|
||||
|
||||
static allowedAttributes = {
|
||||
"expiration-date": "string", // REQUIRED: Date to display
|
||||
"email-address": "string", // Optional: Email address to display
|
||||
};
|
||||
|
||||
render() {
|
||||
const emailAddressText = this.getAttribute("email-address")
|
||||
? `This invitation was sent by <a href="mailto:${this.getAttribute("email-address")}" class="link">${this.getAttribute("email-address")}</a> and expires `
|
||||
: "This invitation expires ";
|
||||
|
||||
return this.renderMJML(
|
||||
`
|
||||
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
|
||||
<mj-column>
|
||||
<mj-text font-size="12px" line-height="24px" padding="10px 15px">
|
||||
${emailAddressText + this.getAttribute("expiration-date")}
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MjBwInviterInfo;
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
<mj-section background-color="#fff" padding="30px 30px 10px 30px">
|
||||
<mj-column>
|
||||
<mj-text font-size="18px" font-weight="700" line-height="32px" padding="0 0 15px 0">
|
||||
Download Bitwarden on all devices
|
||||
</mj-text>
|
||||
<mj-text font-size="15px" line-height="16px" padding="0 0 20px 0">
|
||||
Already using the <a href="https://bitwarden.com/download/" class="link">browser extension</a>?
|
||||
Download the Bitwarden mobile app from the
|
||||
<a href="https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744" class="link">App Store</a>
|
||||
or <a href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden" class="link">Google Play</a>
|
||||
to quickly save logins and autofill forms on the go.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
|
||||
<mj-section background-color="#fff" padding="0 30px 20px 30px">
|
||||
<mj-group>
|
||||
<mj-column width="120px" vertical-align="middle">
|
||||
<mj-image
|
||||
href="https://apps.apple.com/us/app/bitwarden-password-manager/id1137397744"
|
||||
src="https://assets.bitwarden.com/email/v1/App-store.png"
|
||||
alt="Download on the App Store"
|
||||
width="120px"
|
||||
padding="0"
|
||||
/>
|
||||
</mj-column>
|
||||
<mj-column width="150px" vertical-align="middle">
|
||||
<mj-image
|
||||
href="https://play.google.com/store/apps/details?id=com.x8bit.bitwarden"
|
||||
src="https://assets.bitwarden.com/email/v1/google-play-badge.png"
|
||||
alt="Get it on Google Play"
|
||||
width="150px"
|
||||
padding="0 0 0 10px"
|
||||
/>
|
||||
</mj-column>
|
||||
</mj-group>
|
||||
</mj-section>
|
||||
@@ -4,8 +4,8 @@ namespace Bit.Core.Models.Data;
|
||||
|
||||
public class UserKdfInformation
|
||||
{
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public required KdfType Kdf { get; set; }
|
||||
public required int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Settings;
|
||||
using Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public class GlobalSettings : IGlobalSettings
|
||||
{
|
||||
private string _mailTemplateDirectory;
|
||||
private string _logDirectory;
|
||||
private string _licenseDirectory;
|
||||
|
||||
public GlobalSettings()
|
||||
@@ -25,14 +23,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual string KnownProxies { get; set; }
|
||||
public virtual string SiteName { get; set; }
|
||||
public virtual string ProjectName { get; set; }
|
||||
public virtual string LogDirectory
|
||||
{
|
||||
get => BuildDirectory(_logDirectory, "/logs");
|
||||
set => _logDirectory = value;
|
||||
}
|
||||
public virtual bool LogDirectoryByProject { get; set; } = true;
|
||||
public virtual long? LogRollBySizeLimit { get; set; }
|
||||
public virtual bool EnableDevLogging { get; set; } = false;
|
||||
public virtual string LicenseDirectory
|
||||
{
|
||||
get => BuildDirectory(_licenseDirectory, "/core/licenses");
|
||||
@@ -73,9 +63,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual FileStorageSettings Send { get; set; }
|
||||
public virtual IdentityServerSettings IdentityServer { get; set; } = new IdentityServerSettings();
|
||||
public virtual DataProtectionSettings DataProtection { get; set; }
|
||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
||||
public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new();
|
||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||
@@ -548,59 +535,11 @@ public class GlobalSettings : IGlobalSettings
|
||||
}
|
||||
}
|
||||
|
||||
public class SentrySettings
|
||||
{
|
||||
public string Dsn { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationsSettings : ConnectionStringSettings
|
||||
{
|
||||
public string RedisConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public class SyslogSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection string used to connect to a remote syslog server over TCP or UDP, or to connect locally.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The connection string will be parsed using <see cref="System.Uri" /> to extract the protocol, host name and port number.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Supported protocols are:
|
||||
/// <list type="bullet">
|
||||
/// <item>UDP (use <code>udp://</code>)</item>
|
||||
/// <item>TCP (use <code>tcp://</code>)</item>
|
||||
/// <item>TLS over TCP (use <code>tls://</code>)</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// A remote server (logging.dev.example.com) is listening on UDP (port 514):
|
||||
/// <code>
|
||||
/// udp://logging.dev.example.com:514</code>.
|
||||
/// </example>
|
||||
public string Destination { get; set; }
|
||||
/// <summary>
|
||||
/// The absolute path to a Certificate (DER or Base64 encoded with private key).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The certificate path and <see cref="CertificatePassword"/> are passed into the <see cref="System.Security.Cryptography.X509Certificates.X509Certificate2.X509Certificate2(string, string)" />.
|
||||
/// The file format of the certificate may be binary encoded (DER) or base64. If the private key is encrypted, provide the password in <see cref="CertificatePassword"/>,
|
||||
/// </remarks>
|
||||
public string CertificatePath { get; set; }
|
||||
/// <summary>
|
||||
/// The password for the encrypted private key in the certificate supplied in <see cref="CertificatePath" />.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string CertificatePassword { get; set; }
|
||||
/// <summary>
|
||||
/// The thumbprint of the certificate in the X.509 certificate store for personal certificates for the user account running Bitwarden.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
public string CertificateThumbprint { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationHubSettings
|
||||
{
|
||||
private string _connectionString;
|
||||
|
||||
@@ -20,7 +20,6 @@ public interface IGlobalSettings
|
||||
IConnectionStringSettings Storage { get; set; }
|
||||
IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||
ISsoSettings Sso { get; set; }
|
||||
ILogLevelSettings MinLogLevel { get; set; }
|
||||
IPasswordlessAuthSettings PasswordlessAuth { get; set; }
|
||||
IDomainVerificationSettings DomainVerification { get; set; }
|
||||
ILaunchDarklySettings LaunchDarkly { get; set; }
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface ILogLevelSettings
|
||||
{
|
||||
IBillingLogLevelSettings BillingSettings { get; set; }
|
||||
IApiLogLevelSettings ApiSettings { get; set; }
|
||||
IIdentityLogLevelSettings IdentitySettings { get; set; }
|
||||
IScimLogLevelSettings ScimSettings { get; set; }
|
||||
ISsoLogLevelSettings SsoSettings { get; set; }
|
||||
IAdminLogLevelSettings AdminSettings { get; set; }
|
||||
IEventsLogLevelSettings EventsSettings { get; set; }
|
||||
IEventsProcessorLogLevelSettings EventsProcessorSettings { get; set; }
|
||||
IIconsLogLevelSettings IconsSettings { get; set; }
|
||||
INotificationsLogLevelSettings NotificationsSettings { get; set; }
|
||||
}
|
||||
|
||||
public interface IBillingLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel Jobs { get; set; }
|
||||
}
|
||||
|
||||
public interface IApiLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
LogEventLevel IpRateLimit { get; set; }
|
||||
}
|
||||
|
||||
public interface IIdentityLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
LogEventLevel IpRateLimit { get; set; }
|
||||
}
|
||||
|
||||
public interface IScimLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface ISsoLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IAdminLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IEventsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
}
|
||||
|
||||
public interface IEventsProcessorLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface IIconsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
}
|
||||
|
||||
public interface INotificationsLogLevelSettings
|
||||
{
|
||||
LogEventLevel Default { get; set; }
|
||||
LogEventLevel IdentityToken { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class AdminLogLevelSettings : IAdminLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class ApiLogLevelSettings : IApiLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
public LogEventLevel IpRateLimit { get; set; } = LogEventLevel.Information;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class BillingLogLevelSettings : IBillingLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
public LogEventLevel Jobs { get; set; } = LogEventLevel.Information;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class EventsLogLevelSettings : IEventsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class EventsProcessorLogLevelSettings : IEventsProcessorLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class IconsLogLevelSettings : IIconsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class IdentityLogLevelSettings : IIdentityLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
public LogEventLevel IpRateLimit { get; set; } = LogEventLevel.Information;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class LogLevelSettings : ILogLevelSettings
|
||||
{
|
||||
public IBillingLogLevelSettings BillingSettings { get; set; } = new BillingLogLevelSettings();
|
||||
public IApiLogLevelSettings ApiSettings { get; set; } = new ApiLogLevelSettings();
|
||||
public IIdentityLogLevelSettings IdentitySettings { get; set; } = new IdentityLogLevelSettings();
|
||||
public IScimLogLevelSettings ScimSettings { get; set; } = new ScimLogLevelSettings();
|
||||
public ISsoLogLevelSettings SsoSettings { get; set; } = new SsoLogLevelSettings();
|
||||
public IAdminLogLevelSettings AdminSettings { get; set; } = new AdminLogLevelSettings();
|
||||
public IEventsLogLevelSettings EventsSettings { get; set; } = new EventsLogLevelSettings();
|
||||
public IEventsProcessorLogLevelSettings EventsProcessorSettings { get; set; } = new EventsProcessorLogLevelSettings();
|
||||
public IIconsLogLevelSettings IconsSettings { get; set; } = new IconsLogLevelSettings();
|
||||
public INotificationsLogLevelSettings NotificationsSettings { get; set; } = new NotificationsLogLevelSettings();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class NotificationsLogLevelSettings : INotificationsLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
public LogEventLevel IdentityToken { get; set; } = LogEventLevel.Fatal;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class ScimLogLevelSettings : IScimLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Warning;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
public class SsoLogLevelSettings : ISsoLogLevelSettings
|
||||
{
|
||||
public LogEventLevel Default { get; set; } = LogEventLevel.Error;
|
||||
}
|
||||
@@ -1,165 +1,78 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.Syslog;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
public static class LoggerFactoryExtensions
|
||||
{
|
||||
public static void UseSerilog(
|
||||
this IApplicationBuilder appBuilder,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="hostBuilder"></param>
|
||||
/// <returns></returns>
|
||||
public static IHostBuilder AddSerilogFileLogging(this IHostBuilder hostBuilder)
|
||||
{
|
||||
if (env.IsDevelopment() && !globalSettings.EnableDevLogging)
|
||||
return hostBuilder.ConfigureLogging((context, logging) =>
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
applicationLifetime.ApplicationStopped.Register(Log.CloseAndFlush);
|
||||
}
|
||||
|
||||
public static ILoggingBuilder AddSerilog(
|
||||
this ILoggingBuilder builder,
|
||||
WebHostBuilderContext context,
|
||||
Func<LogEvent, IGlobalSettings, bool>? filter = null)
|
||||
{
|
||||
var globalSettings = new GlobalSettings();
|
||||
ConfigurationBinder.Bind(context.Configuration.GetSection("GlobalSettings"), globalSettings);
|
||||
|
||||
if (context.HostingEnvironment.IsDevelopment() && !globalSettings.EnableDevLogging)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
bool inclusionPredicate(LogEvent e)
|
||||
{
|
||||
if (filter == null)
|
||||
if (context.HostingEnvironment.IsDevelopment())
|
||||
{
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
var eventId = e.Properties.TryGetValue("EventId", out var eventIdValue) ? eventIdValue.ToString() : null;
|
||||
if (eventId?.Contains(Constants.BypassFiltersEventId.ToString()) ?? false)
|
||||
|
||||
// If they have begun using the new settings location, use that
|
||||
if (!string.IsNullOrEmpty(context.Configuration["Logging:PathFormat"]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return filter(e, globalSettings);
|
||||
}
|
||||
|
||||
var logSentryWarning = false;
|
||||
var logSyslogWarning = false;
|
||||
|
||||
// Path format is the only required option for file logging, we will use that as
|
||||
// the keystone for if they have configured the new location.
|
||||
var newPathFormat = context.Configuration["Logging:PathFormat"];
|
||||
|
||||
var config = new LoggerConfiguration()
|
||||
.MinimumLevel.Verbose()
|
||||
.Enrich.FromLogContext()
|
||||
.Filter.ByIncludingOnly(inclusionPredicate);
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Sentry.Dsn))
|
||||
{
|
||||
config.WriteTo.Sentry(globalSettings.Sentry.Dsn)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Project", globalSettings.ProjectName);
|
||||
}
|
||||
else if (CoreHelpers.SettingHasValue(globalSettings.Syslog.Destination))
|
||||
{
|
||||
logSyslogWarning = true;
|
||||
// appending sitename to project name to allow easier identification in syslog.
|
||||
var appName = $"{globalSettings.SiteName}-{globalSettings.ProjectName}";
|
||||
if (globalSettings.Syslog.Destination.Equals("local", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
config.WriteTo.LocalSyslog(appName);
|
||||
}
|
||||
else if (Uri.TryCreate(globalSettings.Syslog.Destination, UriKind.Absolute, out var syslogAddress))
|
||||
{
|
||||
// Syslog's standard port is 514 (both UDP and TCP). TLS does not have a standard port, so assume 514.
|
||||
int port = syslogAddress.Port >= 0
|
||||
? syslogAddress.Port
|
||||
: 514;
|
||||
|
||||
if (syslogAddress.Scheme.Equals("udp"))
|
||||
{
|
||||
config.WriteTo.UdpSyslog(syslogAddress.Host, port, appName);
|
||||
}
|
||||
else if (syslogAddress.Scheme.Equals("tcp"))
|
||||
{
|
||||
config.WriteTo.TcpSyslog(syslogAddress.Host, port, appName);
|
||||
}
|
||||
else if (syslogAddress.Scheme.Equals("tls"))
|
||||
{
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Syslog.CertificateThumbprint))
|
||||
{
|
||||
config.WriteTo.TcpSyslog(syslogAddress.Host, port, appName,
|
||||
useTls: true,
|
||||
certProvider: new CertificateStoreProvider(StoreName.My, StoreLocation.CurrentUser,
|
||||
globalSettings.Syslog.CertificateThumbprint));
|
||||
}
|
||||
else
|
||||
{
|
||||
config.WriteTo.TcpSyslog(syslogAddress.Host, port, appName,
|
||||
useTls: true,
|
||||
certProvider: new CertificateFileProvider(globalSettings.Syslog.CertificatePath,
|
||||
globalSettings.Syslog?.CertificatePassword ?? string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(newPathFormat))
|
||||
{
|
||||
// Use new location
|
||||
builder.AddFile(context.Configuration.GetSection("Logging"));
|
||||
}
|
||||
else if (CoreHelpers.SettingHasValue(globalSettings.LogDirectory))
|
||||
{
|
||||
if (globalSettings.LogRollBySizeLimit.HasValue)
|
||||
{
|
||||
var pathFormat = Path.Combine(globalSettings.LogDirectory, $"{globalSettings.ProjectName.ToLowerInvariant()}.log");
|
||||
if (globalSettings.LogDirectoryByProject)
|
||||
{
|
||||
pathFormat = Path.Combine(globalSettings.LogDirectory, globalSettings.ProjectName, "log.txt");
|
||||
}
|
||||
config.WriteTo.File(pathFormat, rollOnFileSizeLimit: true,
|
||||
fileSizeLimitBytes: globalSettings.LogRollBySizeLimit);
|
||||
logging.AddFile(context.Configuration.GetSection("Logging"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathFormat = Path.Combine(globalSettings.LogDirectory, $"{globalSettings.ProjectName.ToLowerInvariant()}_{{Date}}.log");
|
||||
if (globalSettings.LogDirectoryByProject)
|
||||
var globalSettingsSection = context.Configuration.GetSection("GlobalSettings");
|
||||
var loggingOptions = new LegacyFileLoggingOptions();
|
||||
globalSettingsSection.Bind(loggingOptions);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(loggingOptions.LogDirectory))
|
||||
{
|
||||
pathFormat = Path.Combine(globalSettings.LogDirectory, globalSettings.ProjectName, "{Date}.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
var projectName = loggingOptions.ProjectName
|
||||
?? context.HostingEnvironment.ApplicationName;
|
||||
|
||||
if (loggingOptions.LogRollBySizeLimit.HasValue)
|
||||
{
|
||||
var pathFormat = loggingOptions.LogDirectoryByProject
|
||||
? Path.Combine(loggingOptions.LogDirectory, projectName, "log.txt")
|
||||
: Path.Combine(loggingOptions.LogDirectory, $"{projectName.ToLowerInvariant()}.log");
|
||||
|
||||
logging.AddFile(
|
||||
pathFormat: pathFormat,
|
||||
fileSizeLimitBytes: loggingOptions.LogRollBySizeLimit.Value
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var pathFormat = loggingOptions.LogDirectoryByProject
|
||||
? Path.Combine(loggingOptions.LogDirectory, projectName, "{Date}.txt")
|
||||
: Path.Combine(loggingOptions.LogDirectory, $"{projectName.ToLowerInvariant()}_{{Date}}.log");
|
||||
|
||||
logging.AddFile(
|
||||
pathFormat: pathFormat
|
||||
);
|
||||
}
|
||||
config.WriteTo.RollingFile(pathFormat);
|
||||
}
|
||||
config
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Project", globalSettings.ProjectName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var serilog = config.CreateLogger();
|
||||
|
||||
if (logSentryWarning)
|
||||
{
|
||||
serilog.Warning("Sentry for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation");
|
||||
}
|
||||
|
||||
if (logSyslogWarning)
|
||||
{
|
||||
serilog.Warning("Syslog for logging has been deprecated. Read more: https://btwrdn.com/log-deprecation");
|
||||
}
|
||||
|
||||
builder.AddSerilog(serilog);
|
||||
|
||||
return builder;
|
||||
/// <summary>
|
||||
/// Our own proprietary options that we've always supported in `GlobalSettings` configuration section.
|
||||
/// </summary>
|
||||
private class LegacyFileLoggingOptions
|
||||
{
|
||||
public string? ProjectName { get; set; }
|
||||
public string? LogDirectory { get; set; } = "/etc/bitwarden/logs";
|
||||
public bool LogDirectoryByProject { get; set; } = true;
|
||||
public long? LogRollBySizeLimit { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,26 +12,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.EventsSettings.IdentityToken;
|
||||
}
|
||||
|
||||
if (e.Properties.TryGetValue("RequestPath", out var requestPath) &&
|
||||
!string.IsNullOrWhiteSpace(requestPath?.ToString()) &&
|
||||
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.EventsSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -90,11 +90,8 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
"events": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"amazon": {
|
||||
"accessKeyId": "SECRET",
|
||||
"accessKeySecret": "SECRET",
|
||||
|
||||
@@ -11,9 +11,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.EventsProcessorSettings.Default));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
@@ -37,14 +36,9 @@ public class Startup
|
||||
services.AddHostedService<AzureQueueHostedService>();
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
app.UseRouting();
|
||||
|
||||
@@ -11,9 +11,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.IconsSettings.Default));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -60,11 +60,8 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -195,16 +195,35 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
// Moved from API, If you modify this endpoint, please update API as well. Self hosted installs still use the API endpoints.
|
||||
[HttpPost("prelogin")]
|
||||
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
|
||||
[Obsolete("Migrating to use a more descriptive endpoint that would support different types of prelogins. " +
|
||||
"Use prelogin/password instead. This endpoint has no EOL at the time of writing.")]
|
||||
public async Task<PasswordPreloginResponseModel> PostPrelogin([FromBody] PasswordPreloginRequestModel model)
|
||||
{
|
||||
// Same as PostPasswordPrelogin to maintain compatibility. Do not make changes in this function body,
|
||||
// only make changes in MakePasswordPreloginCall
|
||||
return await MakePasswordPreloginCall(model);
|
||||
}
|
||||
|
||||
// There are two functions done this way because the open api docs that get generated in our build pipeline
|
||||
// cannot handle two of the same post attributes on the same function call. That is why there is a
|
||||
// PostPrelogin and the more appropriate PostPasswordPrelogin.
|
||||
[HttpPost("prelogin/password")]
|
||||
public async Task<PasswordPreloginResponseModel> PostPasswordPrelogin([FromBody] PasswordPreloginRequestModel model)
|
||||
{
|
||||
// Same as PostPrelogin to maintain backwards compatibility. Do not make changes in this function body,
|
||||
// only make changes in MakePasswordPreloginCall
|
||||
return await MakePasswordPreloginCall(model);
|
||||
}
|
||||
|
||||
private async Task<PasswordPreloginResponseModel> MakePasswordPreloginCall(PasswordPreloginRequestModel model)
|
||||
{
|
||||
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
|
||||
if (kdfInformation == null)
|
||||
{
|
||||
kdfInformation = GetDefaultKdf(model.Email);
|
||||
}
|
||||
return new PreloginResponseModel(kdfInformation);
|
||||
return new PasswordPreloginResponseModel(kdfInformation, model.Email);
|
||||
}
|
||||
|
||||
[HttpGet("webauthn/assertion-options")]
|
||||
@@ -228,19 +247,17 @@ public class AccountsController : Controller
|
||||
{
|
||||
return _defaultKdfResults[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Compute the HMAC hash of the email
|
||||
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);
|
||||
var hmacHash = hmac.ComputeHash(hmacMessage);
|
||||
// Convert the hash to a number
|
||||
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
|
||||
var hashFirst8Bytes = hashHex.Substring(0, 16);
|
||||
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
|
||||
// Find the default KDF value for this hash number
|
||||
var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);
|
||||
return _defaultKdfResults[hashIndex];
|
||||
}
|
||||
|
||||
// Compute the HMAC hash of the email
|
||||
var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant());
|
||||
using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey);
|
||||
var hmacHash = hmac.ComputeHash(hmacMessage);
|
||||
// Convert the hash to a number
|
||||
var hashHex = BitConverter.ToString(hmacHash).Replace("-", string.Empty).ToLowerInvariant();
|
||||
var hashFirst8Bytes = hashHex.Substring(0, 16);
|
||||
var hashNumber = long.Parse(hashFirst8Bytes, System.Globalization.NumberStyles.HexNumber);
|
||||
// Find the default KDF value for this hash number
|
||||
var hashIndex = (int)(Math.Abs(hashNumber) % _defaultKdfResults.Count);
|
||||
return _defaultKdfResults[hashIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Identity.Models.Request.Accounts;
|
||||
|
||||
public class PreloginRequestModel
|
||||
public class PasswordPreloginRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Identity.Models.Response.Accounts;
|
||||
|
||||
public class PasswordPreloginResponseModel
|
||||
{
|
||||
public PasswordPreloginResponseModel(UserKdfInformation kdfInformation, string? salt = null)
|
||||
{
|
||||
// PM-28143 Cleanup
|
||||
Kdf = kdfInformation.Kdf;
|
||||
KdfIterations = kdfInformation.KdfIterations;
|
||||
KdfMemory = kdfInformation.KdfMemory;
|
||||
KdfParallelism = kdfInformation.KdfParallelism;
|
||||
// End Cleanup
|
||||
|
||||
KdfSettings = new KdfSettings()
|
||||
{
|
||||
KdfType = kdfInformation.Kdf,
|
||||
Iterations = kdfInformation.KdfIterations,
|
||||
Memory = kdfInformation.KdfMemory,
|
||||
Parallelism = kdfInformation.KdfParallelism,
|
||||
};
|
||||
Salt = salt;
|
||||
}
|
||||
|
||||
// Old Data Types
|
||||
public KdfType? Kdf { get; set; } // PM-28143 Remove with cleanup
|
||||
public int? KdfIterations { get; set; } // PM-28143 Remove with cleanup
|
||||
public int? KdfMemory { get; set; } // PM-28143 Remove with cleanup
|
||||
public int? KdfParallelism { get; set; } // PM-28143 Remove with cleanup
|
||||
|
||||
// New Data Types
|
||||
public KdfSettings? KdfSettings { get; set; } // PM-28143 With cleanup make this not nullish
|
||||
public string? Salt { get; set; } // PM-28143 With cleanup make this not nullish. Not used yet,
|
||||
// just the email from the request at this time.
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Identity.Models.Response.Accounts;
|
||||
|
||||
public class PreloginResponseModel
|
||||
{
|
||||
public PreloginResponseModel(UserKdfInformation kdfInformation)
|
||||
{
|
||||
Kdf = kdfInformation.Kdf;
|
||||
KdfIterations = kdfInformation.KdfIterations;
|
||||
KdfMemory = kdfInformation.KdfMemory;
|
||||
KdfParallelism = kdfInformation.KdfParallelism;
|
||||
}
|
||||
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Identity;
|
||||
|
||||
@@ -23,23 +19,7 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.Contains(typeof(IpRateLimitMiddleware).FullName))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.IdentitySettings.IpRateLimit;
|
||||
}
|
||||
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.IdentitySettings.IdentityToken;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.IdentitySettings.Default;
|
||||
}));
|
||||
});
|
||||
})
|
||||
.AddSerilogFileLogging();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,14 +170,11 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<Startup> logger)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -27,9 +27,6 @@
|
||||
"events": {
|
||||
"connectionString": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"notificationHub": {
|
||||
"connectionString": "SECRET",
|
||||
"hubName": "SECRET"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
|
||||
@@ -13,37 +12,8 @@ public class Program
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
webBuilder.ConfigureLogging((hostingContext, logging) =>
|
||||
logging.AddSerilog(hostingContext, (e, globalSettings) =>
|
||||
{
|
||||
var context = e.Properties["SourceContext"].ToString();
|
||||
if (context.Contains("Duende.IdentityServer.Validation.TokenValidator") ||
|
||||
context.Contains("Duende.IdentityServer.Validation.TokenRequestValidator"))
|
||||
{
|
||||
return e.Level >= globalSettings.MinLogLevel.NotificationsSettings.IdentityToken;
|
||||
}
|
||||
|
||||
if (e.Level == LogEventLevel.Error &&
|
||||
e.MessageTemplate.Text == "Failed connection handshake.")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.Level == LogEventLevel.Error &&
|
||||
e.MessageTemplate.Text.StartsWith("Failed writing message."))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.Level == LogEventLevel.Warning &&
|
||||
e.MessageTemplate.Text.StartsWith("Heartbeat took longer"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return e.Level >= globalSettings.MinLogLevel.NotificationsSettings.Default;
|
||||
}));
|
||||
})
|
||||
.AddSerilogFileLogging()
|
||||
.Build()
|
||||
.Run();
|
||||
}
|
||||
|
||||
@@ -82,11 +82,9 @@ public class Startup
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
app.UseSerilog(env, appLifetime, globalSettings);
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
"connectionString": "SECRET",
|
||||
"applicationCacheTopicName": "SECRET"
|
||||
},
|
||||
"sentry": {
|
||||
"dsn": "SECRET"
|
||||
},
|
||||
"amazon": {
|
||||
"accessKeyId": "SECRET",
|
||||
"accessKeySecret": "SECRET",
|
||||
|
||||
@@ -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