1
0
mirror of https://github.com/bitwarden/server synced 2025-12-17 16:53:23 +00:00
Files
server/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
Todd Martin 3668a445e5 chore(docs): Add docs for legacy mail service
* Added docs for legacy mail service.

* Updated namespaces.

* Consolidated under Platform.Mail namespace

* Updated obsolete comment.

* Linting

* Linting

* Replaced documentation in original readme after accidental deletion.
2025-11-04 11:54:39 -05:00

1072 lines
50 KiB
C#

// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Net;
using System.Reflection;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit;
using Azure.Messaging.ServiceBus;
using Bit.Core;
using Bit.Core.AdminConsole.AbilitiesCache;
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.Services;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.LoginFeatures;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.Services.Implementations;
using Bit.Core.Auth.UserFeatures;
using Bit.Core.Auth.UserFeatures.PasswordValidation;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.TrialInitiation;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.HostedServices;
using Bit.Core.KeyManagement;
using Bit.Core.NotificationCenter;
using Bit.Core.OrganizationFeatures;
using Bit.Core.Platform;
using Bit.Core.Platform.Mail.Delivery;
using Bit.Core.Platform.Mail.Enqueuing;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.PushRegistration.Internal;
using Bit.Core.Repositories;
using Bit.Core.Resources;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.SecretsManager.Repositories.Noop;
using Bit.Core.Services;
using Bit.Core.Services.Implementations;
using Bit.Core.Services.Mail;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.SendFeatures;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Bit.Core.Vault;
using Bit.Core.Vault.Services;
using Bit.Infrastructure.Dapper;
using Bit.Infrastructure.EntityFramework;
using DnsClient;
using Duende.IdentityModel;
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Interfaces;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Azure.Cosmos.Fluent;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Extensions.Caching.Cosmos;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using NoopRepos = Bit.Core.Repositories.Noop;
using Role = Bit.Core.Entities.Role;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
namespace Bit.SharedWeb.Utilities;
public static class ServiceCollectionExtensions
{
public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings)
{
var (provider, connectionString) = GetDatabaseProvider(globalSettings);
services.SetupEntityFramework(connectionString, provider);
if (provider != SupportedDatabaseProviders.SqlServer)
{
services.AddPasswordManagerEFRepositories(globalSettings.SelfHosted);
}
else
{
services.AddDapperRepositories(globalSettings.SelfHosted);
}
if (globalSettings.SelfHosted)
{
services.AddSingleton<IInstallationDeviceRepository, NoopRepos.InstallationDeviceRepository>();
}
else
{
services.AddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.AddSingleton<IInstallationDeviceRepository, TableStorageRepos.InstallationDeviceRepository>();
services.AddKeyedSingleton<IGrantRepository, Core.Auth.Repositories.Cosmos.GrantRepository>("cosmos");
}
return provider;
}
public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<ICipherService, CipherService>();
services.AddUserServices(globalSettings);
services.AddTrialInitiationServices();
services.AddOrganizationServices(globalSettings);
services.AddPolicyServices();
services.AddScoped<IGroupService, GroupService>();
services.AddScoped<IEventService, EventService>();
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddSingleton<IDeviceService, DeviceService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<IAuthRequestService, AuthRequestService>();
services.AddScoped<IDuoUniversalTokenService, DuoUniversalTokenService>();
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
services.AddLoginServices();
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
services.AddVaultServices();
services.AddReportingServices();
services.AddKeyManagementServices();
services.AddNotificationCenterServices();
services.AddPlatformServices();
services.AddImportServices();
services.AddSendServices();
}
public static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrgDeleteTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<OrgDeleteTokenable>(
OrgDeleteTokenable.ClearTextPrefix,
OrgDeleteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgDeleteTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<EmergencyAccessInviteTokenable>(
EmergencyAccessInviteTokenable.ClearTextPrefix,
EmergencyAccessInviteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<EmergencyAccessInviteTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<SsoTokenable>(
SsoTokenable.ClearTextPrefix,
SsoTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>(
WebAuthnCredentialCreateOptionsTokenable.ClearTextPrefix,
WebAuthnCredentialCreateOptionsTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>(
WebAuthnLoginAssertionOptionsTokenable.ClearTextPrefix,
WebAuthnLoginAssertionOptionsTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<SsoEmail2faSessionTokenable>(
SsoEmail2faSessionTokenable.ClearTextPrefix,
SsoEmail2faSessionTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<SsoEmail2faSessionTokenable>>>()));
services.AddSingleton<IOrgUserInviteTokenableFactory, OrgUserInviteTokenableFactory>();
services.AddSingleton<IDataProtectorTokenFactory<OrgUserInviteTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<OrgUserInviteTokenable>(
OrgUserInviteTokenable.ClearTextPrefix,
OrgUserInviteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<OrgUserInviteTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<DuoUserStateTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<DuoUserStateTokenable>(
DuoUserStateTokenable.ClearTextPrefix,
DuoUserStateTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<DuoUserStateTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<ProviderDeleteTokenable>>(serviceProvider =>
new DataProtectorTokenFactory<ProviderDeleteTokenable>(
ProviderDeleteTokenable.ClearTextPrefix,
ProviderDeleteTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<ProviderDeleteTokenable>>>())
);
services.AddSingleton<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>(
serviceProvider => new DataProtectorTokenFactory<RegistrationEmailVerificationTokenable>(
RegistrationEmailVerificationTokenable.ClearTextPrefix,
RegistrationEmailVerificationTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>>()));
services.AddSingleton<IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>>(
serviceProvider => new DataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>(
TwoFactorAuthenticatorUserVerificationTokenable.ClearTextPrefix,
TwoFactorAuthenticatorUserVerificationTokenable.DataProtectorPurpose,
serviceProvider.GetDataProtectionProvider(),
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable>>>()));
}
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
{
// Required for UserService
services.AddWebAuthn(globalSettings);
// Required for HTTP calls
services.AddHttpClient();
services.AddSingleton<IStripeAdapter, StripeAdapter>();
services.AddSingleton<Braintree.IBraintreeGateway>((serviceProvider) =>
{
return new Braintree.BraintreeGateway
{
Environment = globalSettings.Braintree.Production ?
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
MerchantId = globalSettings.Braintree.MerchantId,
PublicKey = globalSettings.Braintree.PublicKey,
PrivateKey = globalSettings.Braintree.PrivateKey
};
});
services.AddScoped<IPaymentService, StripePaymentService>();
services.AddScoped<IPaymentHistoryService, PaymentHistoryService>();
services.AddScoped<ITwoFactorEmailService, TwoFactorEmailService>();
// Legacy mailer service
services.AddSingleton<IStripeSyncService, StripeSyncService>();
services.AddSingleton<IMailService, HandlebarsMailService>();
// Modern mailers
services.AddMailer();
services.AddSingleton<ILicensingService, LicensingService>();
services.AddSingleton<ILookupClient>(_ =>
{
var options = new LookupClientOptions { Timeout = TimeSpan.FromSeconds(15), UseTcpOnly = true };
return new LookupClient(options);
});
services.AddSingleton<IDnsResolverService, DnsResolverService>();
services.AddOptionality();
services.AddTokenizers();
services.AddSingleton<IVNextInMemoryApplicationCacheService, VNextInMemoryApplicationCacheService>();
services.AddScoped<IApplicationCacheService, FeatureRoutedCacheService>();
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))
{
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryServiceBusApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, ServiceBusApplicationCacheMessaging>();
}
else
{
services.AddSingleton<IVCurrentInMemoryApplicationCacheService, InMemoryApplicationCacheService>();
services.AddSingleton<IApplicationCacheServiceBusMessaging, NoOpApplicationCacheMessaging>();
}
var awsConfigured = CoreHelpers.SettingHasValue(globalSettings.Amazon?.AccessKeySecret);
if (awsConfigured && CoreHelpers.SettingHasValue(globalSettings.Mail?.SendGridApiKey))
{
services.AddSingleton<IMailDeliveryService, MultiServiceMailDeliveryService>();
}
else if (awsConfigured)
{
services.AddSingleton<IMailDeliveryService, AmazonSesMailDeliveryService>();
}
else if (CoreHelpers.SettingHasValue(globalSettings.Mail?.Smtp?.Host))
{
services.AddSingleton<IMailDeliveryService, MailKitSmtpMailDeliveryService>();
}
else
{
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
}
services.AddPush(globalSettings);
services.AddPushRegistration();
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
{
services.AddSingleton<IMailEnqueuingService, AzureQueueMailService>();
}
else
{
services.AddSingleton<IMailEnqueuingService, BlockingMailEnqueuingService>();
}
services.AddEventWriteServices(globalSettings);
if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))
{
services.AddSingleton<IAttachmentStorageService, AzureAttachmentStorageService>();
}
else if (CoreHelpers.SettingHasValue(globalSettings.Attachment.BaseDirectory))
{
services.AddSingleton<IAttachmentStorageService, LocalAttachmentStorageService>();
}
else
{
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.Send.ConnectionString))
{
services.AddSingleton<ISendFileStorageService, AzureSendFileStorageService>();
}
else if (CoreHelpers.SettingHasValue(globalSettings.Send.BaseDirectory))
{
services.AddSingleton<ISendFileStorageService, LocalSendStorageService>();
}
else
{
services.AddSingleton<ISendFileStorageService, NoopSendFileStorageService>();
}
}
public static void AddOosServices(this IServiceCollection services)
{
services.AddScoped<IProviderService, NoopProviderService>();
services.AddScoped<IServiceAccountRepository, NoopServiceAccountRepository>();
services.AddScoped<ISecretRepository, NoopSecretRepository>();
services.AddScoped<IProjectRepository, NoopProjectRepository>();
}
public static void AddNoopServices(this IServiceCollection services)
{
services.AddSingleton<IMailService, NoopMailService>();
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
services.AddSingleton<ILicensingService, NoopLicensingService>();
}
public static IdentityBuilder AddCustomIdentityServices(
this IServiceCollection services, GlobalSettings globalSettings)
{
services.TryAddTransient(typeof(IOtpTokenProvider<>), typeof(OtpTokenProvider<>));
services.AddScoped<IOrganizationDuoUniversalTokenProvider, OrganizationDuoUniversalTokenProvider>();
services.Configure<PasswordHasherOptions>(options => options.IterationCount = PasswordValidationConstants.PasswordHasherKdfIterations);
services.Configure<TwoFactorRememberTokenProviderOptions>(options =>
{
options.TokenLifespan = TimeSpan.FromDays(30);
});
var identityBuilder = services.AddIdentityWithoutCookieAuth<User, Role>(options =>
{
options.User = new UserOptions
{
RequireUniqueEmail = true,
AllowedUserNameCharacters = null // all
};
options.Password = new PasswordOptions
{
RequireDigit = false,
RequireLowercase = false,
RequiredLength = 12,
RequireNonAlphanumeric = false,
RequireUppercase = false
};
options.ClaimsIdentity = new ClaimsIdentityOptions
{
SecurityStampClaimType = Claims.SecurityStamp,
UserNameClaimType = JwtClaimTypes.Email,
UserIdClaimType = JwtClaimTypes.Subject,
};
options.Tokens.ChangeEmailTokenProvider = TokenOptions.DefaultEmailProvider;
});
identityBuilder
.AddUserStore<UserStore>()
.AddRoleStore<RoleStore>()
.AddTokenProvider<DataProtectorTokenProvider<User>>(TokenOptions.DefaultProvider)
.AddTokenProvider<AuthenticatorTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator))
.AddTokenProvider<EmailTwoFactorTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email))
.AddTokenProvider<YubicoOtpTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey))
.AddTokenProvider<DuoUniversalTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo))
.AddTokenProvider<TwoFactorRememberTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember))
.AddTokenProvider<EmailTokenProvider>(TokenOptions.DefaultEmailProvider)
.AddTokenProvider<WebAuthnTokenProvider>(
CoreHelpers.CustomProviderName(TwoFactorProviderType.WebAuthn));
return identityBuilder;
}
public static void AddIdentityAuthenticationServices(
this IServiceCollection services, GlobalSettings globalSettings, IWebHostEnvironment environment,
Action<AuthorizationOptions> addAuthorization)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.MapInboundClaims = false;
options.Authority = globalSettings.BaseServiceUri.InternalIdentity;
options.RequireHttpsMetadata = !environment.IsDevelopment() &&
globalSettings.BaseServiceUri.InternalIdentity.StartsWith("https");
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
options.TokenValidationParameters.NameClaimType = ClaimTypes.Email;
options.Events = new JwtBearerEvents
{
OnMessageReceived = (context) =>
{
context.Token = TokenRetrieval.FromAuthorizationHeaderOrQueryString()(context.Request);
return Task.CompletedTask;
}
};
});
if (addAuthorization != null)
{
services.AddAuthorization(config =>
{
addAuthorization.Invoke(config);
});
}
if (environment.IsDevelopment())
{
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
}
}
public static void AddCustomDataProtectionServices(
this IServiceCollection services, IWebHostEnvironment env, GlobalSettings globalSettings)
{
var builder = services.AddDataProtection().SetApplicationName("Bitwarden");
if (env.IsDevelopment())
{
return;
}
if (globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.DataProtection.Directory))
{
builder.PersistKeysToFileSystem(new DirectoryInfo(globalSettings.DataProtection.Directory));
}
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Storage?.ConnectionString))
{
X509Certificate2 dataProtectionCert = null;
if (CoreHelpers.SettingHasValue(globalSettings.DataProtection.CertificateThumbprint))
{
dataProtectionCert = CoreHelpers.GetCertificate(
globalSettings.DataProtection.CertificateThumbprint);
}
else if (CoreHelpers.SettingHasValue(globalSettings.DataProtection.CertificatePassword))
{
dataProtectionCert = CoreHelpers.GetBlobCertificateAsync(globalSettings.Storage.ConnectionString, "certificates",
"dataprotection.pfx", globalSettings.DataProtection.CertificatePassword)
.GetAwaiter().GetResult();
}
builder
.PersistKeysToAzureBlobStorage(globalSettings.Storage.ConnectionString, "aspnet-dataprotection", "keys.xml")
.ProtectKeysWithCertificate(dataProtectionCert);
}
}
public static IIdentityServerBuilder AddIdentityServerCertificate(
this IIdentityServerBuilder identityServerBuilder, IWebHostEnvironment env, GlobalSettings globalSettings)
{
var certificate = CoreHelpers.GetIdentityServerCertificate(globalSettings);
if (certificate != null)
{
identityServerBuilder.AddSigningCredential(certificate);
}
else if (env.IsDevelopment() && !string.IsNullOrEmpty(globalSettings.DevelopmentDirectory))
{
var developerSigningKeyPath = Path.Combine(globalSettings.DevelopmentDirectory, "signingkey.jwk");
identityServerBuilder.AddDeveloperSigningCredential(true, developerSigningKeyPath);
}
else if (env.IsDevelopment())
{
identityServerBuilder.AddDeveloperSigningCredential(false);
}
else
{
throw new Exception("No identity certificate to use.");
}
return identityServerBuilder;
}
public static GlobalSettings AddGlobalSettingsServices(this IServiceCollection services,
IConfiguration configuration, IHostEnvironment environment)
{
var globalSettings = new GlobalSettings();
ConfigurationBinder.Bind(configuration.GetSection("GlobalSettings"), globalSettings);
if (environment.IsDevelopment() && configuration.GetValue<bool>("developSelfHosted"))
{
// Override settings with selfHostedOverride settings
ConfigurationBinder.Bind(configuration.GetSection("Dev:SelfHostOverride:GlobalSettings"), globalSettings);
}
services.AddSingleton(s => globalSettings);
services.AddSingleton<IGlobalSettings, GlobalSettings>(s => globalSettings);
return globalSettings;
}
public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
{
services.TryAddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName))
{
services.TryAddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();
services.TryAddKeyedSingleton<IEventWriteService, EventIntegrationEventWriteService>("broadcast");
}
else
{
services.TryAddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
}
}
else if (globalSettings.SelfHosted)
{
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
if (IsRabbitMqEnabled(globalSettings))
{
services.TryAddSingleton<IEventIntegrationPublisher, RabbitMqService>();
services.TryAddKeyedSingleton<IEventWriteService, EventIntegrationEventWriteService>("broadcast");
}
else
{
services.TryAddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
}
}
else
{
services.TryAddKeyedSingleton<IEventWriteService, NoopEventWriteService>("storage");
services.TryAddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
}
services.TryAddScoped<IEventWriteService, EventRouteService>();
return services;
}
public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!IsAzureServiceBusEnabled(globalSettings))
{
return services;
}
services.TryAddSingleton<IAzureServiceBusService, AzureServiceBusService>();
services.TryAddSingleton<IEventIntegrationPublisher, AzureServiceBusService>();
services.TryAddSingleton<IEventRepository, TableStorageRepos.EventRepository>();
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
services.TryAddSingleton<AzureTableStorageEventHandler>();
services.AddEventIntegrationServices(globalSettings);
return services;
}
public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
{
if (!IsRabbitMqEnabled(globalSettings))
{
return services;
}
services.TryAddSingleton<IRabbitMqService, RabbitMqService>();
services.TryAddSingleton<IEventIntegrationPublisher, RabbitMqService>();
services.TryAddSingleton<EventRepositoryHandler>();
services.AddEventIntegrationServices(globalSettings);
return services;
}
public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
{
services.AddHttpClient(SlackService.HttpClientName);
services.TryAddSingleton<ISlackService, SlackService>();
}
else
{
services.TryAddSingleton<ISlackService, NoopSlackService>();
}
return services;
}
public static IServiceCollection AddTeamsService(this IServiceCollection services, GlobalSettings globalSettings)
{
if (CoreHelpers.SettingHasValue(globalSettings.Teams.ClientId) &&
CoreHelpers.SettingHasValue(globalSettings.Teams.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Teams.Scopes))
{
services.AddHttpClient(TeamsService.HttpClientName);
services.TryAddSingleton<TeamsService>();
services.TryAddSingleton<IBot>(sp => sp.GetRequiredService<TeamsService>());
services.TryAddSingleton<ITeamsService>(sp => sp.GetRequiredService<TeamsService>());
services.TryAddSingleton<IBotFrameworkHttpAdapter>(sp =>
new BotFrameworkHttpAdapter(
new TeamsBotCredentialProvider(
clientId: globalSettings.Teams.ClientId,
clientSecret: globalSettings.Teams.ClientSecret
)
)
);
}
else
{
services.TryAddSingleton<ITeamsService, NoopTeamsService>();
}
return services;
}
public static void UseDefaultMiddleware(this IApplicationBuilder app,
IWebHostEnvironment env, GlobalSettings globalSettings)
{
app.UseMiddleware<RequestLoggingMiddleware>();
}
public static void UseForwardedHeaders(this IApplicationBuilder app, IGlobalSettings globalSettings)
{
var options = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
};
if (!globalSettings.UnifiedDeployment)
{
// Trust the X-Forwarded-Host header of the nginx docker container
try
{
var nginxIp = Dns.GetHostEntry("nginx")?.AddressList.FirstOrDefault();
if (nginxIp != null)
{
options.KnownProxies.Add(nginxIp);
}
}
catch
{
// Ignore DNS errors
}
}
if (!string.IsNullOrWhiteSpace(globalSettings.KnownProxies))
{
var proxies = globalSettings.KnownProxies.Split(',');
foreach (var proxy in proxies)
{
if (IPAddress.TryParse(proxy.Trim(), out var ip))
{
options.KnownProxies.Add(ip);
}
}
}
if (options.KnownProxies.Count > 1)
{
options.ForwardLimit = null;
}
app.UseForwardedHeaders(options);
}
public static void AddCoreLocalizationServices(this IServiceCollection services)
{
services.AddTransient<II18nService, I18nService>();
services.AddLocalization(options => options.ResourcesPath = "Resources");
}
public static IApplicationBuilder UseCoreLocalization(this IApplicationBuilder app)
{
var supportedCultures = new[] { "en" };
return app.UseRequestLocalization(options => options
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures));
}
public static IMvcBuilder AddViewAndDataAnnotationLocalization(this IMvcBuilder mvc)
{
mvc.Services.AddTransient<IViewLocalizer, I18nViewLocalizer>();
return mvc.AddViewLocalization(options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization(options =>
options.DataAnnotationLocalizerProvider = (type, factory) =>
{
var assemblyName = new AssemblyName(typeof(SharedResources).GetTypeInfo().Assembly.FullName);
return factory.Create("SharedResources", assemblyName.Name);
});
}
public static IServiceCollection AddDistributedIdentityServices(this IServiceCollection services)
{
services.AddOidcStateDataFormatterCache();
services.AddSession();
services.ConfigureApplicationCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());
services.ConfigureExternalCookie(configure => configure.CookieManager = new DistributedCacheCookieManager());
services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, ConfigureOpenIdConnectDistributedOptions>();
return services;
}
public static void AddWebAuthn(this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddFido2(options =>
{
options.ServerDomain = new Uri(globalSettings.BaseServiceUri.Vault).Host;
options.ServerName = "Bitwarden";
options.TimestampDriftTolerance = 300000;
if (globalSettings.Fido2?.Origins?.Any() == true)
{
options.Origins = new HashSet<string>(globalSettings.Fido2.Origins);
}
else
{
// Default to allowing the vault domain and chromium browser extension IDs
options.Origins = new HashSet<string> {
globalSettings.BaseServiceUri.Vault,
Constants.BrowserExtensions.ChromeId,
Constants.BrowserExtensions.EdgeId,
Constants.BrowserExtensions.OperaId
};
}
});
}
/// <summary>
/// Adds either an in-memory or distributed IP rate limiter depending if a Redis connection string is available.
/// </summary>
/// <param name="services"></param>
/// <param name="globalSettings"></param>
public static void AddIpRateLimiting(this IServiceCollection services,
GlobalSettings globalSettings)
{
services.AddHostedService<IpRateLimitSeedStartupService>();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();
if (!globalSettings.DistributedIpRateLimiting.Enabled ||
string.IsNullOrEmpty(globalSettings.DistributedIpRateLimiting.RedisConnectionString))
{
services.AddInMemoryRateLimiting();
}
else
{
// Use memory stores for Ip and Client Policy stores as we don't currently use them
// and they add unnecessary Redis network delays checking for policies that don't exist
services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
// Use a custom Redis processing strategy that skips Ip limiting if Redis is down
services.AddKeyedSingleton<IConnectionMultiplexer>("rate-limiter", (_, provider) =>
ConnectionMultiplexer.Connect(globalSettings.DistributedIpRateLimiting.RedisConnectionString));
services.AddSingleton<IProcessingStrategy, CustomRedisProcessingStrategy>();
}
}
/// <summary>
/// Adds an implementation of <see cref="IDistributedCache"/> to the service collection. Uses a memory
/// cache if self hosted or no Redis connection string is available in GlobalSettings.
/// </summary>
public static void AddDistributedCache(
this IServiceCollection services,
GlobalSettings globalSettings)
{
if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Redis?.ConnectionString))
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = globalSettings.DistributedCache.Redis.ConnectionString;
});
}
else
{
var (databaseProvider, databaseConnectionString) = GetDatabaseProvider(globalSettings);
if (databaseProvider == SupportedDatabaseProviders.SqlServer)
{
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = databaseConnectionString;
o.SchemaName = "dbo";
o.TableName = "Cache";
});
}
else
{
services.AddSingleton<IDistributedCache, EntityFrameworkCache>();
}
}
if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Cosmos?.ConnectionString))
{
services.AddKeyedSingleton<IDistributedCache>("persistent", (s, _) =>
new CosmosCache(new CosmosCacheOptions
{
DatabaseName = "cache",
ContainerName = "default",
CreateIfNotExists = false,
ClientBuilder = new CosmosClientBuilder(globalSettings.DistributedCache?.Cosmos?.ConnectionString)
}));
}
else
{
services.AddKeyedSingleton("persistent", (s, _) => s.GetRequiredService<IDistributedCache>());
}
}
public static IServiceCollection AddOptionality(this IServiceCollection services)
{
services.AddSingleton<ILdClient>(s =>
{
return new LdClient(LaunchDarklyFeatureService.GetConfiguredClient(
s.GetRequiredService<GlobalSettings>()));
});
services.AddScoped<IFeatureService, LaunchDarklyFeatureService>();
return services;
}
private static (SupportedDatabaseProviders provider, string connectionString)
GetDatabaseProvider(GlobalSettings globalSettings)
{
var selectedDatabaseProvider = globalSettings.DatabaseProvider;
var provider = SupportedDatabaseProviders.SqlServer;
var connectionString = string.Empty;
if (!string.IsNullOrWhiteSpace(selectedDatabaseProvider))
{
switch (selectedDatabaseProvider.ToLowerInvariant())
{
case "postgres":
case "postgresql":
provider = SupportedDatabaseProviders.Postgres;
connectionString = globalSettings.PostgreSql.ConnectionString;
break;
case "mysql":
case "mariadb":
provider = SupportedDatabaseProviders.MySql;
connectionString = globalSettings.MySql.ConnectionString;
break;
case "sqlite":
provider = SupportedDatabaseProviders.Sqlite;
connectionString = globalSettings.Sqlite.ConnectionString;
break;
case "sqlserver":
connectionString = globalSettings.SqlServer.ConnectionString;
break;
default:
break;
}
}
else
{
// Default to attempting to use SqlServer connection string if globalSettings.DatabaseProvider has no value.
connectionString = globalSettings.SqlServer.ConnectionString;
}
return (provider, connectionString);
}
private static IServiceCollection AddAzureServiceBusIntegration<TConfig, TListenerConfig>(this IServiceCollection services,
TListenerConfig listenerConfiguration)
where TConfig : class
where TListenerConfig : IIntegrationListenerConfiguration
{
services.TryAddKeyedSingleton<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
new EventIntegrationHandler<TConfig>(
integrationType: listenerConfiguration.IntegrationType,
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
userRepository: provider.GetRequiredService<IUserRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
AzureServiceBusEventListenerService<TListenerConfig>>(provider =>
new AzureServiceBusEventListenerService<TListenerConfig>(
configuration: listenerConfiguration,
handler: provider.GetRequiredKeyedService<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey),
serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = listenerConfiguration.EventPrefetchCount,
MaxConcurrentCalls = listenerConfiguration.EventMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService<ILoggerFactory>()
)
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
AzureServiceBusIntegrationListenerService<TListenerConfig>>(provider =>
new AzureServiceBusIntegrationListenerService<TListenerConfig>(
configuration: listenerConfiguration,
handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),
serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = listenerConfiguration.IntegrationPrefetchCount,
MaxConcurrentCalls = listenerConfiguration.IntegrationMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService<ILoggerFactory>()
)
)
);
return services;
}
private static IServiceCollection AddEventIntegrationServices(this IServiceCollection services,
GlobalSettings globalSettings)
{
// Add common services
services.TryAddSingleton<IntegrationConfigurationDetailsCacheService>();
services.TryAddSingleton<IIntegrationConfigurationDetailsCache>(provider =>
provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
services.AddHostedService(provider => provider.GetRequiredService<IntegrationConfigurationDetailsCacheService>());
services.TryAddSingleton<IIntegrationFilterService, IntegrationFilterService>();
services.TryAddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
// Add services in support of handlers
services.AddSlackService(globalSettings);
services.AddTeamsService(globalSettings);
services.TryAddSingleton(TimeProvider.System);
services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
services.AddHttpClient(DatadogIntegrationHandler.HttpClientName);
// Add integration handlers
services.TryAddSingleton<IIntegrationHandler<SlackIntegrationConfigurationDetails>, SlackIntegrationHandler>();
services.TryAddSingleton<IIntegrationHandler<WebhookIntegrationConfigurationDetails>, WebhookIntegrationHandler>();
services.TryAddSingleton<IIntegrationHandler<DatadogIntegrationConfigurationDetails>, DatadogIntegrationHandler>();
services.TryAddSingleton<IIntegrationHandler<TeamsIntegrationConfigurationDetails>, TeamsIntegrationHandler>();
var repositoryConfiguration = new RepositoryListenerConfiguration(globalSettings);
var slackConfiguration = new SlackListenerConfiguration(globalSettings);
var webhookConfiguration = new WebhookListenerConfiguration(globalSettings);
var hecConfiguration = new HecListenerConfiguration(globalSettings);
var datadogConfiguration = new DatadogListenerConfiguration(globalSettings);
var teamsConfiguration = new TeamsListenerConfiguration(globalSettings);
if (IsRabbitMqEnabled(globalSettings))
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
RabbitMqEventListenerService<RepositoryListenerConfiguration>>(provider =>
new RabbitMqEventListenerService<RepositoryListenerConfiguration>(
handler: provider.GetRequiredService<EventRepositoryHandler>(),
configuration: repositoryConfiguration,
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
loggerFactory: provider.GetRequiredService<ILoggerFactory>()
)
)
);
services.AddRabbitMqIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);
services.AddRabbitMqIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);
services.AddRabbitMqIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);
services.AddRabbitMqIntegration<TeamsIntegrationConfigurationDetails, TeamsListenerConfiguration>(teamsConfiguration);
}
if (IsAzureServiceBusEnabled(globalSettings))
{
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
AzureServiceBusEventListenerService<RepositoryListenerConfiguration>>(provider =>
new AzureServiceBusEventListenerService<RepositoryListenerConfiguration>(
configuration: repositoryConfiguration,
handler: provider.GetRequiredService<AzureTableStorageEventHandler>(),
serviceBusService: provider.GetRequiredService<IAzureServiceBusService>(),
serviceBusOptions: new ServiceBusProcessorOptions()
{
PrefetchCount = repositoryConfiguration.EventPrefetchCount,
MaxConcurrentCalls = repositoryConfiguration.EventMaxConcurrentCalls
},
loggerFactory: provider.GetRequiredService<ILoggerFactory>()
)
)
);
services.AddAzureServiceBusIntegration<SlackIntegrationConfigurationDetails, SlackListenerConfiguration>(slackConfiguration);
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, WebhookListenerConfiguration>(webhookConfiguration);
services.AddAzureServiceBusIntegration<WebhookIntegrationConfigurationDetails, HecListenerConfiguration>(hecConfiguration);
services.AddAzureServiceBusIntegration<DatadogIntegrationConfigurationDetails, DatadogListenerConfiguration>(datadogConfiguration);
services.AddAzureServiceBusIntegration<TeamsIntegrationConfigurationDetails, TeamsListenerConfiguration>(teamsConfiguration);
}
return services;
}
private static IServiceCollection AddRabbitMqIntegration<TConfig, TListenerConfig>(this IServiceCollection services,
TListenerConfig listenerConfiguration)
where TConfig : class
where TListenerConfig : IIntegrationListenerConfiguration
{
services.TryAddKeyedSingleton<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey, implementationFactory: (provider, _) =>
new EventIntegrationHandler<TConfig>(
integrationType: listenerConfiguration.IntegrationType,
eventIntegrationPublisher: provider.GetRequiredService<IEventIntegrationPublisher>(),
integrationFilterService: provider.GetRequiredService<IIntegrationFilterService>(),
configurationCache: provider.GetRequiredService<IIntegrationConfigurationDetailsCache>(),
userRepository: provider.GetRequiredService<IUserRepository>(),
organizationRepository: provider.GetRequiredService<IOrganizationRepository>(),
logger: provider.GetRequiredService<ILogger<EventIntegrationHandler<TConfig>>>()
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
RabbitMqEventListenerService<TListenerConfig>>(provider =>
new RabbitMqEventListenerService<TListenerConfig>(
handler: provider.GetRequiredKeyedService<IEventMessageHandler>(serviceKey: listenerConfiguration.RoutingKey),
configuration: listenerConfiguration,
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
loggerFactory: provider.GetRequiredService<ILoggerFactory>()
)
)
);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
RabbitMqIntegrationListenerService<TListenerConfig>>(provider =>
new RabbitMqIntegrationListenerService<TListenerConfig>(
handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),
configuration: listenerConfiguration,
rabbitMqService: provider.GetRequiredService<IRabbitMqService>(),
loggerFactory: provider.GetRequiredService<ILoggerFactory>(),
timeProvider: provider.GetRequiredService<TimeProvider>()
)
)
);
return services;
}
private static bool IsAzureServiceBusEnabled(GlobalSettings settings)
{
return CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(settings.EventLogging.AzureServiceBus.EventTopicName);
}
private static bool IsRabbitMqEnabled(GlobalSettings settings)
{
return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
}
}