From 62705917ab2391d2f9805c3616087204207f761c Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:12:33 -0600 Subject: [PATCH 1/5] Remove unreferenced FF (#6612) --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c4f8f6458e..b0669091b9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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"; From 042279682aaf843e1c5e57342a37775915e6dfa2 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:38:59 -0800 Subject: [PATCH 2/5] feat(marketing-initiated-premium): (Auth) [PM-27540] Add optional Marketing Property to RegisterSendVerificationEmailRequestModel (#6598) Adds an optional `FromMarketing` property to the RegisterSendVerificationEmailRequestModel. --- .../MarketingInitiativeValidationAttribute.cs | 29 ++++++++ .../Accounts/MarketingInitiativeConstants.cs | 10 +++ ...gisterSendVerificationEmailRequestModel.cs | 3 + ...etingInitiativeValidationAttributeTests.cs | 70 +++++++++++++++++++ ...rketingInitiativeConstantsSnapshotTests.cs | 18 +++++ 5 files changed, 130 insertions(+) create mode 100644 src/Core/Auth/Attributes/MarketingInitiativeValidationAttribute.cs create mode 100644 src/Core/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstants.cs create mode 100644 test/Core.Test/Auth/Attributes/MarketingInitiativeValidationAttributeTests.cs create mode 100644 test/Core.Test/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstantsSnapshotTests.cs diff --git a/src/Core/Auth/Attributes/MarketingInitiativeValidationAttribute.cs b/src/Core/Auth/Attributes/MarketingInitiativeValidationAttribute.cs new file mode 100644 index 0000000000..bcc4b851c0 --- /dev/null +++ b/src/Core/Auth/Attributes/MarketingInitiativeValidationAttribute.cs @@ -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); + } +} diff --git a/src/Core/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstants.cs b/src/Core/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstants.cs new file mode 100644 index 0000000000..ab2d252dc8 --- /dev/null +++ b/src/Core/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstants.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Auth.Models.Api.Request.Accounts; + +public static class MarketingInitiativeConstants +{ + /// + /// 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. + /// + public const string Premium = "premium"; +} diff --git a/src/Core/Auth/Models/Api/Request/Accounts/RegisterSendVerificationEmailRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/RegisterSendVerificationEmailRequestModel.cs index 75a4da081a..638565ecfe 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/RegisterSendVerificationEmailRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/RegisterSendVerificationEmailRequestModel.cs @@ -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; } } diff --git a/test/Core.Test/Auth/Attributes/MarketingInitiativeValidationAttributeTests.cs b/test/Core.Test/Auth/Attributes/MarketingInitiativeValidationAttributeTests.cs new file mode 100644 index 0000000000..2b9b5cf194 --- /dev/null +++ b/test/Core.Test/Auth/Attributes/MarketingInitiativeValidationAttributeTests.cs @@ -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); + } +} diff --git a/test/Core.Test/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstantsSnapshotTests.cs b/test/Core.Test/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstantsSnapshotTests.cs new file mode 100644 index 0000000000..b78e96e91e --- /dev/null +++ b/test/Core.Test/Auth/Models/Api/Request/Accounts/MarketingInitiativeConstantsSnapshotTests.cs @@ -0,0 +1,18 @@ +using Bit.Core.Auth.Models.Api.Request.Accounts; +using Xunit; + +namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts; + +/// +/// Snapshot tests to ensure the string constants in do not change unintentionally. +/// If you intentionally change any of these values, please update the tests to reflect the new expected values. +/// +public class MarketingInitiativeConstantsSnapshotTests +{ + [Fact] + public void MarketingInitiativeConstants_HaveCorrectValues() + { + // Assert + Assert.Equal("premium", MarketingInitiativeConstants.Premium); + } +} From fdfec0ac4df89b33896aadf532e95404f01747c0 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:39:26 -0500 Subject: [PATCH 3/5] Remove deprecated logging methods (#6516) --- .github/renovate.json5 | 4 - bitwarden_license/src/Scim/Program.cs | 15 +- bitwarden_license/src/Scim/Startup.cs | 3 - bitwarden_license/src/Scim/appsettings.json | 3 - bitwarden_license/src/Sso/Program.cs | 14 +- bitwarden_license/src/Sso/Startup.cs | 2 - src/Admin/Program.cs | 13 +- src/Admin/Startup.cs | 3 - src/Api/Program.cs | 33 +-- src/Api/Startup.cs | 2 - src/Api/appsettings.json | 3 - src/Billing/Program.cs | 19 +- src/Billing/Startup.cs | 7 +- src/Billing/appsettings.json | 3 - src/Core/Core.csproj | 4 - src/Core/Settings/GlobalSettings.cs | 61 ------ src/Core/Settings/IGlobalSettings.cs | 1 - src/Core/Settings/ILogLevelSettings.cs | 74 ------- .../LoggingSettings/AdminLogLevelSettings.cs | 8 - .../LoggingSettings/ApiLogLevelSettings.cs | 10 - .../BillingLogLevelSettings.cs | 9 - .../LoggingSettings/EventsLogLevelSettings.cs | 9 - .../EventsProcessorLogLevelSettings.cs | 8 - .../LoggingSettings/IconsLogLevelSettings.cs | 8 - .../IdentityLogLevelSettings.cs | 10 - .../LoggingSettings/LogLevelSettings.cs | 16 -- .../NotificationsLogLevelSettings.cs | 9 - .../LoggingSettings/ScimLogLevelSettings.cs | 8 - .../LoggingSettings/SsoLogLevelSettings.cs | 8 - src/Core/Utilities/LoggerFactoryExtensions.cs | 199 +++++------------- src/Events/Program.cs | 20 +- src/Events/Startup.cs | 3 - src/Events/appsettings.json | 3 - src/EventsProcessor/Program.cs | 3 +- src/EventsProcessor/Startup.cs | 8 +- src/Icons/Program.cs | 3 +- src/Icons/Startup.cs | 3 - src/Identity/Program.cs | 26 +-- src/Identity/Startup.cs | 3 - src/Identity/appsettings.json | 3 - src/Notifications/Program.cs | 32 +-- src/Notifications/Startup.cs | 2 - src/Notifications/appsettings.json | 3 - .../Utilities/LoggerFactoryExtensionsTests.cs | 103 ++------- 44 files changed, 93 insertions(+), 688 deletions(-) delete mode 100644 src/Core/Settings/ILogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/AdminLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/BillingLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/EventsLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/EventsProcessorLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/IconsLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/IdentityLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/LogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/NotificationsLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/ScimLogLevelSettings.cs delete mode 100644 src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e892e59b22..6a23a7e832 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -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", diff --git a/bitwarden_license/src/Scim/Program.cs b/bitwarden_license/src/Scim/Program.cs index 92f12f59dd..02f2e00d32 100644 --- a/bitwarden_license/src/Scim/Program.cs +++ b/bitwarden_license/src/Scim/Program.cs @@ -11,21 +11,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - 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(); } diff --git a/bitwarden_license/src/Scim/Startup.cs b/bitwarden_license/src/Scim/Startup.cs index edbbf34aea..2a84faa8dd 100644 --- a/bitwarden_license/src/Scim/Startup.cs +++ b/bitwarden_license/src/Scim/Startup.cs @@ -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(); diff --git a/bitwarden_license/src/Scim/appsettings.json b/bitwarden_license/src/Scim/appsettings.json index dcdfeb3ede..18b7a7ca7b 100644 --- a/bitwarden_license/src/Scim/appsettings.json +++ b/bitwarden_license/src/Scim/appsettings.json @@ -30,9 +30,6 @@ "connectionString": "SECRET", "applicationCacheTopicName": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "notificationHub": { "connectionString": "SECRET", "hubName": "SECRET" diff --git a/bitwarden_license/src/Sso/Program.cs b/bitwarden_license/src/Sso/Program.cs index 1a8ce6eb88..bac3bb3d13 100644 --- a/bitwarden_license/src/Sso/Program.cs +++ b/bitwarden_license/src/Sso/Program.cs @@ -1,5 +1,4 @@ using Bit.Core.Utilities; -using Serilog; namespace Bit.Sso; @@ -13,19 +12,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - 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(); } diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 3ae8883ac4..2f83f3dad0 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -100,8 +100,6 @@ public class Startup IdentityModelEventSource.ShowPII = true; } - app.UseSerilog(env, appLifetime, globalSettings); - // Add general security headers app.UseMiddleware(); diff --git a/src/Admin/Program.cs b/src/Admin/Program.cs index 05bf35d41d..006a8223b2 100644 --- a/src/Admin/Program.cs +++ b/src/Admin/Program.cs @@ -16,19 +16,8 @@ public class Program o.Limits.MaxRequestLineSize = 20_000; }); webBuilder.UseStartup(); - 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(); } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 5ecbdc899c..87d68a7ac6 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -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(); diff --git a/src/Api/Program.cs b/src/Api/Program.cs index 6023f51c6d..bf924af47f 100644 --- a/src/Api/Program.cs +++ b/src/Api/Program.cs @@ -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(); - 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(); } diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 0967b4f662..8ecdd148d3 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -234,12 +234,10 @@ public class Startup public void Configure( IApplicationBuilder app, IWebHostEnvironment env, - IHostApplicationLifetime appLifetime, GlobalSettings globalSettings, ILogger logger) { IdentityModelEventSource.ShowPII = true; - app.UseSerilog(env, appLifetime, globalSettings); // Add general security headers app.UseMiddleware(); diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index 98bb4df8ac..a503070d8d 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -32,9 +32,6 @@ "send": { "connectionString": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "notificationHub": { "connectionString": "SECRET", "hubName": "SECRET" diff --git a/src/Billing/Program.cs b/src/Billing/Program.cs index 3e005ce7fd..72ff6072c5 100644 --- a/src/Billing/Program.cs +++ b/src/Billing/Program.cs @@ -11,25 +11,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - 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(); } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index cdb9700ad5..1343dc0895 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -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(); diff --git a/src/Billing/appsettings.json b/src/Billing/appsettings.json index a2d6acd0a1..aa14f1d377 100644 --- a/src/Billing/appsettings.json +++ b/src/Billing/appsettings.json @@ -30,9 +30,6 @@ "connectionString": "SECRET", "applicationCacheTopicName": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "notificationHub": { "connectionString": "SECRET", "hubName": "SECRET" diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index cfd10f0917..1be6e52854 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -50,13 +50,9 @@ - - - - diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 147b88623a..3446d1af2a 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -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 - { - /// - /// The connection string used to connect to a remote syslog server over TCP or UDP, or to connect locally. - /// - /// - /// The connection string will be parsed using to extract the protocol, host name and port number. - /// - /// - /// Supported protocols are: - /// - /// UDP (use udp://) - /// TCP (use tcp://) - /// TLS over TCP (use tls://) - /// - /// - /// - /// - /// A remote server (logging.dev.example.com) is listening on UDP (port 514): - /// - /// udp://logging.dev.example.com:514. - /// - public string Destination { get; set; } - /// - /// The absolute path to a Certificate (DER or Base64 encoded with private key). - /// - /// - /// The certificate path and are passed into the . - /// The file format of the certificate may be binary encoded (DER) or base64. If the private key is encrypted, provide the password in , - /// - public string CertificatePath { get; set; } - /// - /// The password for the encrypted private key in the certificate supplied in . - /// - /// - public string CertificatePassword { get; set; } - /// - /// The thumbprint of the certificate in the X.509 certificate store for personal certificates for the user account running Bitwarden. - /// - /// - public string CertificateThumbprint { get; set; } - } - public class NotificationHubSettings { private string _connectionString; diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index 0fc99d63e3..20b832c678 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -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; } diff --git a/src/Core/Settings/ILogLevelSettings.cs b/src/Core/Settings/ILogLevelSettings.cs deleted file mode 100644 index b3cedf083c..0000000000 --- a/src/Core/Settings/ILogLevelSettings.cs +++ /dev/null @@ -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; } -} diff --git a/src/Core/Settings/LoggingSettings/AdminLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/AdminLogLevelSettings.cs deleted file mode 100644 index d2c74dd076..0000000000 --- a/src/Core/Settings/LoggingSettings/AdminLogLevelSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Events; - -namespace Bit.Core.Settings.LoggingSettings; - -public class AdminLogLevelSettings : IAdminLogLevelSettings -{ - public LogEventLevel Default { get; set; } = LogEventLevel.Error; -} diff --git a/src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs deleted file mode 100644 index 7961ab7e3b..0000000000 --- a/src/Core/Settings/LoggingSettings/ApiLogLevelSettings.cs +++ /dev/null @@ -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; -} diff --git a/src/Core/Settings/LoggingSettings/BillingLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/BillingLogLevelSettings.cs deleted file mode 100644 index b9e53e6bca..0000000000 --- a/src/Core/Settings/LoggingSettings/BillingLogLevelSettings.cs +++ /dev/null @@ -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; -} diff --git a/src/Core/Settings/LoggingSettings/EventsLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/EventsLogLevelSettings.cs deleted file mode 100644 index 3201748550..0000000000 --- a/src/Core/Settings/LoggingSettings/EventsLogLevelSettings.cs +++ /dev/null @@ -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; -} diff --git a/src/Core/Settings/LoggingSettings/EventsProcessorLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/EventsProcessorLogLevelSettings.cs deleted file mode 100644 index 5aff18a216..0000000000 --- a/src/Core/Settings/LoggingSettings/EventsProcessorLogLevelSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Events; - -namespace Bit.Core.Settings.LoggingSettings; - -public class EventsProcessorLogLevelSettings : IEventsProcessorLogLevelSettings -{ - public LogEventLevel Default { get; set; } = LogEventLevel.Warning; -} diff --git a/src/Core/Settings/LoggingSettings/IconsLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/IconsLogLevelSettings.cs deleted file mode 100644 index c7b73ba687..0000000000 --- a/src/Core/Settings/LoggingSettings/IconsLogLevelSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Events; - -namespace Bit.Core.Settings.LoggingSettings; - -public class IconsLogLevelSettings : IIconsLogLevelSettings -{ - public LogEventLevel Default { get; set; } = LogEventLevel.Error; -} diff --git a/src/Core/Settings/LoggingSettings/IdentityLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/IdentityLogLevelSettings.cs deleted file mode 100644 index a823cb5109..0000000000 --- a/src/Core/Settings/LoggingSettings/IdentityLogLevelSettings.cs +++ /dev/null @@ -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; -} diff --git a/src/Core/Settings/LoggingSettings/LogLevelSettings.cs b/src/Core/Settings/LoggingSettings/LogLevelSettings.cs deleted file mode 100644 index 1af05ebfde..0000000000 --- a/src/Core/Settings/LoggingSettings/LogLevelSettings.cs +++ /dev/null @@ -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(); -} diff --git a/src/Core/Settings/LoggingSettings/NotificationsLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/NotificationsLogLevelSettings.cs deleted file mode 100644 index 3494fbfcca..0000000000 --- a/src/Core/Settings/LoggingSettings/NotificationsLogLevelSettings.cs +++ /dev/null @@ -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; -} diff --git a/src/Core/Settings/LoggingSettings/ScimLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/ScimLogLevelSettings.cs deleted file mode 100644 index f297b17e95..0000000000 --- a/src/Core/Settings/LoggingSettings/ScimLogLevelSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Events; - -namespace Bit.Core.Settings.LoggingSettings; - -public class ScimLogLevelSettings : IScimLogLevelSettings -{ - public LogEventLevel Default { get; set; } = LogEventLevel.Warning; -} diff --git a/src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs b/src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs deleted file mode 100644 index 495ec41fd0..0000000000 --- a/src/Core/Settings/LoggingSettings/SsoLogLevelSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Serilog.Events; - -namespace Bit.Core.Settings.LoggingSettings; - -public class SsoLogLevelSettings : ISsoLogLevelSettings -{ - public LogEventLevel Default { get; set; } = LogEventLevel.Error; -} diff --git a/src/Core/Utilities/LoggerFactoryExtensions.cs b/src/Core/Utilities/LoggerFactoryExtensions.cs index 54bd84df6f..b950e30d5d 100644 --- a/src/Core/Utilities/LoggerFactoryExtensions.cs +++ b/src/Core/Utilities/LoggerFactoryExtensions.cs @@ -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) + /// + /// + /// + /// + /// + 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? 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; + /// + /// Our own proprietary options that we've always supported in `GlobalSettings` configuration section. + /// + 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; } } } diff --git a/src/Events/Program.cs b/src/Events/Program.cs index 967e94ed83..1a00549005 100644 --- a/src/Events/Program.cs +++ b/src/Events/Program.cs @@ -12,26 +12,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - 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(); } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index cfe177aa2c..f67debd092 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -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(); diff --git a/src/Events/appsettings.json b/src/Events/appsettings.json index e72b978f2f..41637c8549 100644 --- a/src/Events/appsettings.json +++ b/src/Events/appsettings.json @@ -14,9 +14,6 @@ "events": { "connectionString": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "amazon": { "accessKeyId": "SECRET", "accessKeySecret": "SECRET", diff --git a/src/EventsProcessor/Program.cs b/src/EventsProcessor/Program.cs index 9b7a31e6f4..e4f4ac90d1 100644 --- a/src/EventsProcessor/Program.cs +++ b/src/EventsProcessor/Program.cs @@ -11,9 +11,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - webBuilder.ConfigureLogging((hostingContext, logging) => - logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.EventsProcessorSettings.Default)); }) + .AddSerilogFileLogging() .Build() .Run(); } diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs index 67676a8afc..260c501e01 100644 --- a/src/EventsProcessor/Startup.cs +++ b/src/EventsProcessor/Startup.cs @@ -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(); } - 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(); app.UseRouting(); diff --git a/src/Icons/Program.cs b/src/Icons/Program.cs index 237096b0b1..80c1b5728e 100644 --- a/src/Icons/Program.cs +++ b/src/Icons/Program.cs @@ -11,9 +11,8 @@ public class Program .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - webBuilder.ConfigureLogging((hostingContext, logging) => - logging.AddSerilog(hostingContext, (e, globalSettings) => e.Level >= globalSettings.MinLogLevel.IconsSettings.Default)); }) + .AddSerilogFileLogging() .Build() .Run(); } diff --git a/src/Icons/Startup.cs b/src/Icons/Startup.cs index 2602dd6264..5d9b5e5a30 100644 --- a/src/Icons/Startup.cs +++ b/src/Icons/Startup.cs @@ -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(); diff --git a/src/Identity/Program.cs b/src/Identity/Program.cs index cb6e7daf39..238ad8ce3a 100644 --- a/src/Identity/Program.cs +++ b/src/Identity/Program.cs @@ -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(); - 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(); } } diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 74344977a0..5dc443a73c 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -170,14 +170,11 @@ public class Startup public void Configure( IApplicationBuilder app, IWebHostEnvironment env, - IHostApplicationLifetime appLifetime, GlobalSettings globalSettings, ILogger logger) { IdentityModelEventSource.ShowPII = true; - app.UseSerilog(env, appLifetime, globalSettings); - // Add general security headers app.UseMiddleware(); diff --git a/src/Identity/appsettings.json b/src/Identity/appsettings.json index 16c3efe46b..c21d2dff3b 100644 --- a/src/Identity/appsettings.json +++ b/src/Identity/appsettings.json @@ -27,9 +27,6 @@ "events": { "connectionString": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "notificationHub": { "connectionString": "SECRET", "hubName": "SECRET" diff --git a/src/Notifications/Program.cs b/src/Notifications/Program.cs index 072c2404c4..2792391729 100644 --- a/src/Notifications/Program.cs +++ b/src/Notifications/Program.cs @@ -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(); - 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(); } diff --git a/src/Notifications/Startup.cs b/src/Notifications/Startup.cs index 2889e90d3b..65904ea698 100644 --- a/src/Notifications/Startup.cs +++ b/src/Notifications/Startup.cs @@ -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(); diff --git a/src/Notifications/appsettings.json b/src/Notifications/appsettings.json index 020d98cbd6..e36ec02dad 100644 --- a/src/Notifications/appsettings.json +++ b/src/Notifications/appsettings.json @@ -18,9 +18,6 @@ "connectionString": "SECRET", "applicationCacheTopicName": "SECRET" }, - "sentry": { - "dsn": "SECRET" - }, "amazon": { "accessKeyId": "SECRET", "accessKeySecret": "SECRET", diff --git a/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs b/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs index 06bb362336..81311cb802 100644 --- a/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs +++ b/test/Core.Test/Utilities/LoggerFactoryExtensionsTests.cs @@ -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 - { - { "GlobalSettings:EnableDevLogging", "true" }, - }, "Development"); - - var provider = Assert.Single(providers); - Assert.IsAssignableFrom(provider); - } - [Fact] public void AddSerilog_IsProduction_AddsSerilog() { @@ -52,7 +36,7 @@ public class LoggerFactoryExtensionsTests var providers = GetProviders(new Dictionary { { "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 - { - { "GlobalSettings:SysLog:Destination", "tcp://127.0.0.1:25000" }, - { "GlobalSettings:SiteName", "TestSite" }, - { "GlobalSettings:ProjectName", "TestProject" }, - }, "Production"); - - var loggerFactory = provider.GetRequiredService(); - 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 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 GetProviders(Dictionary initialData, string environment = "Production") { var provider = GetServiceProvider(initialData, environment); @@ -172,23 +102,34 @@ public class LoggerFactoryExtensionsTests .AddInMemoryCollection(initialData) .Build(); - var hostingEnvironment = Substitute.For(); + var hostingEnvironment = Substitute.For(); hostingEnvironment .EnvironmentName .Returns(environment); - var context = new WebHostBuilderContext + var context = new HostBuilderContext(new Dictionary()) { HostingEnvironment = hostingEnvironment, Configuration = config, }; var services = new ServiceCollection(); - services.AddLogging(builder => - { - builder.AddSerilog(context); - }); + + var hostBuilder = Substitute.For(); + hostBuilder + .When(h => h.ConfigureServices(Arg.Any>())) + .Do(call => + { + var configureAction = call.Arg>(); + configureAction(context, services); + }); + + hostBuilder.AddSerilogFileLogging(); + + hostBuilder + .ConfigureServices(Arg.Any>()) + .Received(1); return services.BuildServiceProvider(); } From fcc879bd233b0d7fd32926b4b1b573ebb57bb1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:41:01 +0000 Subject: [PATCH 4/5] [PM-28252] Add new feature flag for increasing bulk reinvite limit for cloud (#6624) --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index b0669091b9..0e5e7bf3ca 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -143,6 +143,7 @@ public static class FeatureFlagKeys public const string AccountRecoveryCommand = "pm-25581-prevent-provider-account-recovery"; public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration"; public const string PolicyValidatorsRefactor = "pm-26423-refactor-policy-side-effects"; + public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud"; /* Architecture */ public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1"; From 9573cab37ecbe6cda39b05f5c1063a6710582234 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:30:45 -0500 Subject: [PATCH 5/5] Add template properties for Datadog (#6528) * Add template properites for Datadog * Add test and implementation for including User and ActingUser when only the Type is referenced * Refactored database calls to fetch the user details in a single DB call * Refactor to use a dedicated stored procedure for Dapper * Remove TOP 1 from stored procedure * Accept Claude's optimization of SingleOrDefaultAsync to unify Dapper/EF Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * Revert earlier change and add TOP 1 back into stored procedure * Change go to GO * Revert back to version that assumes uniqueness, remove TOP 1 --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../IntegrationTemplateContext.cs | 11 ++- .../IOrganizationUserRepository.cs | 11 +++ .../EventIntegrationHandler.cs | 28 +++++-- .../Utilities/IntegrationTemplateProcessor.cs | 18 ++++- .../OrganizationUserRepository.cs | 17 ++++ .../OrganizationUserRepository.cs | 16 ++++ .../Utilities/ServiceCollectionExtensions.cs | 7 +- ...UserDetails_ReadByOrganizationIdUserId.sql | 17 ++++ .../IntegrationTemplateContextTests.cs | 58 ++++++++++++-- .../Services/EventIntegrationHandlerTests.cs | 78 +++++++++++++------ .../IntegrationTemplateProcessorTests.cs | 21 +++++ ...UserDetails_ReadByOrganizationIdUserId.sql | 17 ++++ 12 files changed, 259 insertions(+), 40 deletions(-) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql create mode 100644 util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql diff --git a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs index fe33c45156..c44e550d15 100644 --- a/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContext.cs @@ -1,8 +1,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.AdminConsole.Models.Data.EventIntegrations; @@ -36,13 +36,18 @@ public class IntegrationTemplateContext(EventMessage eventMessage) public string DateIso8601 => Date.ToString("o"); public string EventMessage => JsonSerializer.Serialize(Event); - public User? User { get; set; } + public OrganizationUserUserDetails? User { get; set; } public string? UserName => User?.Name; public string? UserEmail => User?.Email; + public OrganizationUserType? UserType => User?.Type; - public User? ActingUser { get; set; } + public OrganizationUserUserDetails? ActingUser { get; set; } public string? ActingUserName => ActingUser?.Name; public string? ActingUserEmail => ActingUser?.Email; + public OrganizationUserType? ActingUserType => ActingUser?.Type; + + public Group? Group { get; set; } + public string? GroupName => Group?.Name; public Organization? Organization { get; set; } public string? OrganizationName => Organization?.DisplayName(); diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index bedb9d49ee..41622c24b7 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -97,4 +97,15 @@ public interface IOrganizationUserRepository : IRepositoryAccepted OrganizationUser to confirm /// True, if the user was updated. False, if not performed. Task ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm); + + /// + /// Returns the OrganizationUserUserDetails if found. + /// + /// The id of the organization + /// The id of the User to fetch + /// OrganizationUserUserDetails of the specified user or null if not found + /// + /// Similar to GetByOrganizationAsync, but returns the user details. + /// + Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId); } diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs index 8423652eb8..e29d0eaaad 100644 --- a/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrations/EventIntegrationHandler.cs @@ -1,5 +1,6 @@ using System.Text.Json; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -13,8 +14,9 @@ public class EventIntegrationHandler( IEventIntegrationPublisher eventIntegrationPublisher, IIntegrationFilterService integrationFilterService, IIntegrationConfigurationDetailsCache configurationCache, - IUserRepository userRepository, + IGroupRepository groupRepository, IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, ILogger> logger) : IEventMessageHandler { @@ -89,19 +91,35 @@ public class EventIntegrationHandler( { var context = new IntegrationTemplateContext(eventMessage); + if (IntegrationTemplateProcessor.TemplateRequiresGroup(template) && eventMessage.GroupId.HasValue) + { + context.Group = await groupRepository.GetByIdAsync(eventMessage.GroupId.Value); + } + + if (eventMessage.OrganizationId is not Guid organizationId) + { + return context; + } + if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) { - context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); + context.User = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( + organizationId: organizationId, + userId: eventMessage.UserId.Value + ); } if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) { - context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + context.ActingUser = await organizationUserRepository.GetDetailsByOrganizationIdUserIdAsync( + organizationId: organizationId, + userId: eventMessage.ActingUserId.Value + ); } - if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) + if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template)) { - context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); + context.Organization = await organizationRepository.GetByIdAsync(organizationId); } return context; diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index b561e58a86..62df3b2bc9 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -26,7 +26,7 @@ public static partial class IntegrationTemplateProcessor return match.Value; // Return unknown keys as keys - i.e. #Key# } - return property?.GetValue(values)?.ToString() ?? ""; + return property.GetValue(values)?.ToString() ?? string.Empty; }); } @@ -38,7 +38,8 @@ public static partial class IntegrationTemplateProcessor } return template.Contains("#UserName#", StringComparison.Ordinal) - || template.Contains("#UserEmail#", StringComparison.Ordinal); + || template.Contains("#UserEmail#", StringComparison.Ordinal) + || template.Contains("#UserType#", StringComparison.Ordinal); } public static bool TemplateRequiresActingUser(string template) @@ -49,7 +50,18 @@ public static partial class IntegrationTemplateProcessor } return template.Contains("#ActingUserName#", StringComparison.Ordinal) - || template.Contains("#ActingUserEmail#", StringComparison.Ordinal); + || template.Contains("#ActingUserEmail#", StringComparison.Ordinal) + || template.Contains("#ActingUserType#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresGroup(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#GroupName#", StringComparison.Ordinal); } public static bool TemplateRequiresOrganization(string template) diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index af52021ca7..eaa3675c47 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -688,4 +688,21 @@ public class OrganizationUserRepository : Repository, IO return rowCount > 0; } + + public async Task GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.QuerySingleOrDefaultAsync( + "[dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId]", + new + { + OrganizationId = organizationId, + UserId = userId + }, + commandType: CommandType.StoredProcedure); + + return result; + } + } } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index fd31b1f0dc..ae55099775 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -965,4 +965,20 @@ public class OrganizationUserRepository : Repository GetDetailsByOrganizationIdUserIdAsync(Guid organizationId, Guid userId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var view = new OrganizationUserUserDetailsViewQuery(); + var entity = await view.Run(dbContext).SingleOrDefaultAsync(ou => ou.OrganizationId == organizationId && ou.UserId == userId); + return entity; + } + } +#nullable disable + + } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index a0c064a2b1..9caa37b997 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; using Bit.Core.AdminConsole.Models.Teams; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.NoopImplementations; @@ -889,8 +890,9 @@ public static class ServiceCollectionExtensions eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), - userRepository: provider.GetRequiredService(), + groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), + organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>() ) ); @@ -1016,8 +1018,9 @@ public static class ServiceCollectionExtensions eventIntegrationPublisher: provider.GetRequiredService(), integrationFilterService: provider.GetRequiredService(), configurationCache: provider.GetRequiredService(), - userRepository: provider.GetRequiredService(), + groupRepository: provider.GetRequiredService(), organizationRepository: provider.GetRequiredService(), + organizationUserRepository: provider.GetRequiredService(), logger: provider.GetRequiredService>>() ) ); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql new file mode 100644 index 0000000000..6113664b76 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationUserUserDetailsView] +WHERE + [OrganizationId] = @OrganizationId +AND + [UserId] = @UserId +END +GO diff --git a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs index cdb109e285..d9a3cd6e8a 100644 --- a/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/EventIntegrations/IntegrationTemplateContextTests.cs @@ -2,8 +2,8 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Entities; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -35,7 +35,7 @@ public class IntegrationTemplateContextTests } [Theory, BitAutoData] - public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user) + public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails user) { var sut = new IntegrationTemplateContext(eventMessage) { User = user }; @@ -51,7 +51,7 @@ public class IntegrationTemplateContextTests } [Theory, BitAutoData] - public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user) + public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails user) { var sut = new IntegrationTemplateContext(eventMessage) { User = user }; @@ -67,7 +67,23 @@ public class IntegrationTemplateContextTests } [Theory, BitAutoData] - public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser) + public void UserType_WhenUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails user) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = user }; + + Assert.Equal(user.Type, sut.UserType); + } + + [Theory, BitAutoData] + public void UserType_WhenUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { User = null }; + + Assert.Null(sut.UserType); + } + + [Theory, BitAutoData] + public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails actingUser) { var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; @@ -83,7 +99,7 @@ public class IntegrationTemplateContextTests } [Theory, BitAutoData] - public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser) + public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails actingUser) { var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; @@ -98,6 +114,22 @@ public class IntegrationTemplateContextTests Assert.Null(sut.ActingUserEmail); } + [Theory, BitAutoData] + public void ActingUserType_WhenActingUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails actingUser) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser }; + + Assert.Equal(actingUser.Type, sut.ActingUserType); + } + + [Theory, BitAutoData] + public void ActingUserType_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null }; + + Assert.Null(sut.ActingUserType); + } + [Theory, BitAutoData] public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization) { @@ -113,4 +145,20 @@ public class IntegrationTemplateContextTests Assert.Null(sut.OrganizationName); } + + [Theory, BitAutoData] + public void GroupName_WhenGroupIsSet_ReturnsName(EventMessage eventMessage, Group group) + { + var sut = new IntegrationTemplateContext(eventMessage) { Group = group }; + + Assert.Equal(group.Name, sut.GroupName); + } + + [Theory, BitAutoData] + public void GroupName_WhenGroupIsNull_ReturnsNull(EventMessage eventMessage) + { + var sut = new IntegrationTemplateContext(eventMessage) { Group = null }; + + Assert.Null(sut.GroupName); + } } diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs index 1d94d58aa5..c556c1fae0 100644 --- a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -1,10 +1,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.EventIntegrations; -using Bit.Core.Entities; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; @@ -20,9 +21,11 @@ namespace Bit.Core.Test.Services; public class EventIntegrationHandlerTests { private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; + private const string _templateWithGroup = "Group: #GroupName#"; private const string _templateWithOrganization = "Org: #OrganizationName#"; - private const string _templateWithUser = "#UserName#, #UserEmail#"; - private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#"; + private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#"; + private static readonly Guid _groupId = Guid.NewGuid(); private static readonly Guid _organizationId = Guid.NewGuid(); private static readonly Uri _uri = new Uri("https://localhost"); private static readonly Uri _uri2 = new Uri("https://example.com"); @@ -45,7 +48,7 @@ public class EventIntegrationHandlerTests .Create(); } - private static IntegrationMessage expectedMessage(string template) + private static IntegrationMessage ExpectedMessage(string template) { return new IntegrationMessage() { @@ -105,7 +108,7 @@ public class EventIntegrationHandlerTests config.Configuration = null; config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri }); config.Template = _templateBase; - config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { }); + config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup()); return [config]; } @@ -138,15 +141,16 @@ public class EventIntegrationHandlerTests await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -157,7 +161,7 @@ public class EventIntegrationHandlerTests await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( @@ -167,29 +171,56 @@ public class EventIntegrationHandlerTests await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); - var user = Substitute.For(); + var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; eventMessage.OrganizationId = _organizationId; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency() + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.ActingUserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup)); + var group = Substitute.For(); + group.Name = "Test"; + eventMessage.GroupId = _groupId; + eventMessage.OrganizationId = _organizationId; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(group); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Group: {group.Name}"); + + Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); + await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( + AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] @@ -205,34 +236,37 @@ public class EventIntegrationHandlerTests Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) { var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); - var user = Substitute.For(); + var user = Substitute.For(); user.Email = "test@example.com"; user.Name = "Test"; eventMessage.OrganizationId = _organizationId; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + sutProvider.GetDependency() + .GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), Arg.Any()).Returns(user); await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}"); Assert.Single(_eventIntegrationPublisher.ReceivedCalls()); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" }))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); + await sutProvider.GetDependency().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any(), eventMessage.UserId ?? Guid.Empty); } [Theory, BitAutoData] @@ -256,7 +290,7 @@ public class EventIntegrationHandlerTests await sutProvider.Sut.HandleEventAsync(eventMessage); - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); @@ -298,7 +332,7 @@ public class EventIntegrationHandlerTests foreach (var eventMessage in eventMessages) { - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is( @@ -316,7 +350,7 @@ public class EventIntegrationHandlerTests foreach (var eventMessage in eventMessages) { - var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" ); await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual( diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index d9df9486b6..aee4af346c 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -83,6 +83,7 @@ public class IntegrationTemplateProcessorTests [Theory] [InlineData("User name is #UserName#")] [InlineData("Email: #UserEmail#")] + [InlineData("User type = #UserType#")] public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template) { var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); @@ -102,6 +103,7 @@ public class IntegrationTemplateProcessorTests [Theory] [InlineData("Acting user is #ActingUserName#")] [InlineData("Acting user's email is #ActingUserEmail#")] + [InlineData("Acting user's type is #ActingUserType#")] public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template) { var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); @@ -118,6 +120,25 @@ public class IntegrationTemplateProcessorTests Assert.False(result); } + [Theory] + [InlineData("Group name is #GroupName#!")] + [InlineData("Group: #GroupName#")] + public void TemplateRequiresGroup_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template); + Assert.True(result); + } + + [Theory] + [InlineData("#GroupId#")] // This is on the base class, not fetched, so should be false + [InlineData("No Group Tokens")] + [InlineData("")] + public void TemplateRequiresGroup_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template); + Assert.False(result); + } + [Theory] [InlineData("Organization: #OrganizationName#")] [InlineData("Welcome to #OrganizationName#")] diff --git a/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql b/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql new file mode 100644 index 0000000000..7c9395bb74 --- /dev/null +++ b/util/Migrator/DbScripts/2025-11-05_00_OrganizationUserUserDetails_ReadByOrganizationIdUserId.sql @@ -0,0 +1,17 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadByOrganizationIdUserId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + * +FROM + [dbo].[OrganizationUserUserDetailsView] +WHERE + [OrganizationId] = @OrganizationId +AND + [UserId] = @UserId +END +GO