elsa-core/src/apps/Elsa.Server.Web/Program.cs
Sipke Schoorstra a5cc3fc9e8
Refactor Tenant Resolution to Use Async Local Storage for Operation-wide Access (#6022)
* Remove obsolete tenant-related classes and add ASP.NET Core middleware

Refactored tenant resolution by removing obsolete interfaces and classes, such as `IAmbientTenantAccessor` and `ITenantResolutionStrategy`. Introduced new ASP.NET Core middleware for tenant resolution, encapsulated in the new `Elsa.Tenants.AspNetCore` project. Updated related usage in various parts of the application to align with these changes.

* Remove HttpContextTenantResolver.

Removed HttpContextTenantResolver from the multitenancy pipeline and related service registrations. This simplifies the tenant resolution by relying on remaining resolvers like ClaimsTenantResolver and RoutePrefixTenantResolver.

* Add Elsa solution definition file

This commit introduces the main solution file, Elsa.slnx, defining the folder structure, projects, and configuration for the Elsa repository. This includes folders for Docker, documentation, pipelines, samples, scripts, source code, and tests.

* Refactor DefaultAccessTokenIssuer for clarity and efficiency

Refactored the DefaultAccessTokenIssuer class by simplifying its constructor and utilizing scoped variables for token options. Improved token creation logic by adding a dedicated method to configure token options, enhancing code readability and maintainability.

* Remove Elsa.slnx solution file

No dotnet build support yet.

* Refactor tenant resolver service registrations

Updated the service registrations to use interfaces for DefaultTenantResolver and DefaultTenantResolverPipelineInvoker. This improves the code's flexibility, making it easier to replace or extend these implementations in the future.

* Add multitenancy support and tenant scope management

Introduced ITenantScopeFactory and related implementations for tenant scope management across the application. Enhanced the HTTP workflows middleware to handle tenants and updated relevant configurations and extension methods to support tenant resolution.

* Remove unnecessary folder inclusion

The <Folder> tag for "Modules\Modules\" was redundant and has been removed to clean up the project file. This change will not affect the existing functionality or project structure.

* Rename Create to CreateScope and improve authorization.

Updated the method name from Create to CreateScope for better clarity in the TenantScopeFactory. Fixed a logical error in the authorization process, ensuring proper status code setting for unauthorized requests, and refactored token expiration calculation for clarity.

* Add tenant agnostic filters and remove tenant setup

This commit introduces tenant agnostic filters in AutoUpdateTests to ensure workflows can trigger regardless of tenant. Additionally, it removes tenant configuration from WorkflowServer setup as it is no longer required for the current tests.
2024-10-12 12:08:09 +02:00

538 lines
22 KiB
C#

using System.Text.Encodings.Web;
using Elsa.Agents;
using Elsa.Alterations.Extensions;
using Elsa.Alterations.MassTransit.Extensions;
using Elsa.Common.DistributedLocks.Noop;
using Elsa.Dapper.Extensions;
using Elsa.Dapper.Services;
using Elsa.DropIns.Extensions;
using Elsa.EntityFrameworkCore.Extensions;
using Elsa.EntityFrameworkCore.Modules.Alterations;
using Elsa.EntityFrameworkCore.Modules.Identity;
using Elsa.EntityFrameworkCore.Modules.Management;
using Elsa.EntityFrameworkCore.Modules.Runtime;
using Elsa.Extensions;
using Elsa.Features.Services;
using Elsa.Http.Multitenancy;
using Elsa.Identity.Multitenancy;
using Elsa.MassTransit.Extensions;
using Elsa.MongoDb.Extensions;
using Elsa.MongoDb.Modules.Alterations;
using Elsa.MongoDb.Modules.Identity;
using Elsa.MongoDb.Modules.Management;
using Elsa.MongoDb.Modules.Runtime;
using Elsa.OpenTelemetry.Middleware;
using Elsa.Secrets.Extensions;
using Elsa.Secrets.Persistence;
using Elsa.Server.Web;
using Elsa.Server.Web.Filters;
using Elsa.Tenants.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Api;
using Elsa.Workflows.Management.Compression;
using Elsa.Workflows.Management.Stores;
using Elsa.Workflows.Runtime.Distributed.Extensions;
using Elsa.Workflows.Runtime.Stores;
using JetBrains.Annotations;
using Medallion.Threading.FileSystem;
using Medallion.Threading.Postgres;
using Medallion.Threading.Redis;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
using Proto.Cluster.Kubernetes;
using Proto.Persistence.Sqlite;
using Proto.Persistence.SqlServer;
using Proto.Remote;
using Proto.Remote.GrpcNet;
using StackExchange.Redis;
// ReSharper disable RedundantAssignment
const PersistenceProvider persistenceProvider = PersistenceProvider.EntityFrameworkCore;
const SqlDatabaseProvider sqlDatabaseProvider = SqlDatabaseProvider.Sqlite;
const bool useHangfire = false;
const bool useQuartz = true;
const bool useMassTransit = true;
const bool useZipCompression = false;
const bool runEFCoreMigrations = true;
const bool useMemoryStores = false;
const bool useCaching = true;
const bool useAzureServiceBusModule = false;
const bool useReadOnlyMode = false;
const bool useSignalR = true;
const WorkflowRuntime workflowRuntime = WorkflowRuntime.ProtoActor;
const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.ProtoActor;
const MassTransitBroker massTransitBroker = MassTransitBroker.Memory;
const bool useMultitenancy = true;
const bool useAgents = true;
const bool useSecrets = true;
const bool useAzureServiceBus = false;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
var configuration = builder.Configuration;
var identitySection = configuration.GetSection("Identity");
var identityTokenSection = identitySection.GetSection("Tokens");
var sqliteConnectionString = configuration.GetConnectionString("Sqlite")!;
var sqlServerConnectionString = configuration.GetConnectionString("SqlServer")!;
var postgresConnectionString = configuration.GetConnectionString("PostgreSql")!;
var cockroachDbConnectionString = configuration.GetConnectionString("CockroachDb")!;
var mongoDbConnectionString = configuration.GetConnectionString("MongoDb")!;
var azureServiceBusConnectionString = configuration.GetConnectionString("AzureServiceBus")!;
var rabbitMqConnectionString = configuration.GetConnectionString("RabbitMq")!;
var redisConnectionString = configuration.GetConnectionString("Redis")!;
var distributedLockProviderName = configuration.GetSection("Runtime:DistributedLocking")["Provider"];
var appRole = Enum.Parse<ApplicationRole>(configuration["AppRole"] ?? "Default");
// Add Elsa services.
services
.AddElsa(elsa =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
elsa.UseMongoDb(mongoDbConnectionString);
if (persistenceProvider == PersistenceProvider.Dapper)
elsa.UseDapper(dapper =>
{
dapper.UseMigrations(feature =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
feature.UseSqlServer();
else
feature.UseSqlite();
});
dapper.DbConnectionProvider = sp =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
return new SqlServerDbConnectionProvider(sqlServerConnectionString!);
else
return new SqliteDbConnectionProvider(sqliteConnectionString);
};
});
if (useHangfire)
elsa.UseHangfire();
elsa
.AddActivitiesFrom<Program>()
.AddWorkflowsFrom<Program>()
.UseFluentStorageProvider()
.UseFileStorage()
.UseIdentity(identity =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
identity.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
identity.UseDapper();
else
identity.UseEntityFrameworkCore(ef =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
ef.UseSqlite(sqliteConnectionString);
ef.RunMigrations = runEFCoreMigrations;
});
identity.TokenOptions = options => identityTokenSection.Bind(options);
identity.UseConfigurationBasedUserProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedApplicationProvider(options => identitySection.Bind(options));
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseWorkflows(workflows =>
{
workflows.WithDefaultWorkflowExecutionPipeline(pipeline => pipeline.UseWorkflowExecutionTracing());
workflows.WithDefaultActivityExecutionPipeline(pipeline => pipeline.UseActivityExecutionTracing());
})
.UseWorkflowManagement(management =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
management.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
management.UseDapper();
else
management.UseEntityFrameworkCore(ef =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
ef.UseSqlite(sqliteConnectionString);
ef.RunMigrations = runEFCoreMigrations;
});
if (useZipCompression)
management.SetCompressionAlgorithm(nameof(Zstd));
if (useMemoryStores)
management.UseWorkflowInstances(feature => feature.WorkflowInstanceStore = sp => sp.GetRequiredService<MemoryWorkflowInstanceStore>());
if (useMassTransit)
management.UseMassTransitDispatcher();
if (useCaching)
management.UseCache();
management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit);
management.UseReadOnlyMode(useReadOnlyMode);
})
.UseProtoActor(proto =>
{
proto
.EnableMetrics()
.EnableTracing();
proto.PersistenceProvider = _ =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
return new SqlServerProvider(sqlServerConnectionString!, true, "", "proto_actor");
return new SqliteProvider(new SqliteConnectionStringBuilder(sqliteConnectionString));
};
if (configuration["KUBERNETES_SERVICE_HOST"] != null)
{
var kubernetesConfig = new KubernetesProviderConfig();
var clusterProvider = new KubernetesProvider(kubernetesConfig);
var remoteConfig = GrpcNetRemoteConfig
.BindToAllInterfaces(advertisedHost: configuration["ProtoActor:AdvertisedHost"]) // Environment variable to be provided by Kubernetes using pod.status.podIP.
.WithLogLevelForDeserializationErrors(LogLevel.Critical)
.WithRemoteDiagnostics(true);
proto.CreateClusterProvider = _ => clusterProvider;
proto.ConfigureRemoteConfig = _ => remoteConfig;
}
})
.UseWorkflowRuntime(runtime =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
runtime.UseMongoDb();
else if (persistenceProvider == PersistenceProvider.Dapper)
runtime.UseDapper();
else
runtime.UseEntityFrameworkCore(ef =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString!);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
ef.UseSqlite(sqliteConnectionString);
ef.RunMigrations = runEFCoreMigrations;
});
if (workflowRuntime == WorkflowRuntime.Distributed)
{
runtime.UseDistributedRuntime();
}
if (workflowRuntime == WorkflowRuntime.ProtoActor)
{
runtime.UseProtoActor();
}
if (useMassTransit)
runtime.UseMassTransitDispatcher();
runtime.WorkflowDispatcherOptions = options => configuration.GetSection("Runtime:WorkflowDispatcher").Bind(options);
if (useMemoryStores)
{
runtime.ActivityExecutionLogStore = sp => sp.GetRequiredService<MemoryActivityExecutionStore>();
runtime.WorkflowExecutionLogStore = sp => sp.GetRequiredService<MemoryWorkflowExecutionLogStore>();
}
if (useCaching)
runtime.UseCache();
runtime.DistributedLockingOptions = options => configuration.GetSection("Runtime:DistributedLocking").Bind(options);
runtime.DistributedLockProvider = _ =>
{
switch (distributedLockProviderName)
{
case "Postgres":
return new PostgresDistributedSynchronizationProvider(postgresConnectionString, options =>
{
options.KeepaliveCadence(TimeSpan.FromMinutes(5));
options.UseMultiplexing();
});
case "Redis":
{
var connectionMultiplexer = ConnectionMultiplexer.Connect(redisConnectionString);
var database = connectionMultiplexer.GetDatabase();
return new RedisDistributedSynchronizationProvider(database);
}
case "File":
return new FileDistributedSynchronizationProvider(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "locks")));
case "Noop":
default:
return new NoopDistributedSynchronizationProvider();
}
};
})
.UseEnvironments(environments => environments.EnvironmentsOptions = options => configuration.GetSection("Environments").Bind(options))
.UseScheduling(scheduling =>
{
if (useHangfire)
scheduling.UseHangfireScheduler();
if (useQuartz)
scheduling.UseQuartzScheduler();
})
.UseWorkflowsApi(api =>
{
api.AddFastEndpointsAssembly<Program>();
})
.UseCSharp(options =>
{
options.AppendScript("string Greet(string name) => $\"Hello {name}!\";");
options.AppendScript("string SayHelloWorld() => Greet(\"World\");");
})
.UseJavaScript(options =>
{
options.AllowClrAccess = true;
options.ConfigureEngine(engine =>
{
engine.Execute("function greet(name) { return `Hello ${name}!`; }");
engine.Execute("function sayHelloWorld() { return greet('World'); }");
});
})
.UsePython(python =>
{
python.PythonOptions += options =>
{
// Make sure to configure the path to the python DLL. E.g. /opt/homebrew/Cellar/python@3.11/3.11.6_1/Frameworks/Python.framework/Versions/3.11/bin/python3.11
// alternatively, you can set the PYTHONNET_PYDLL environment variable.
configuration.GetSection("Scripting:Python").Bind(options);
};
})
.UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default)
.UseHttp(http =>
{
http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options);
if (useCaching)
http.UseCache();
})
.UseEmail(email => email.ConfigureOptions = options => configuration.GetSection("Smtp").Bind(options))
.UseAlterations(alterations =>
{
if (persistenceProvider == PersistenceProvider.MongoDb)
{
alterations.UseMongoDb();
}
else if (persistenceProvider == PersistenceProvider.Dapper)
{
// TODO: alterations.UseDapper();
}
else
{
alterations.UseEntityFrameworkCore(ef =>
{
if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer)
ef.UseSqlServer(sqlServerConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql)
ef.UsePostgreSql(postgresConnectionString);
else if (sqlDatabaseProvider == SqlDatabaseProvider.CockroachDb)
ef.UsePostgreSql(cockroachDbConnectionString!);
else
ef.UseSqlite(sqliteConnectionString);
ef.RunMigrations = runEFCoreMigrations;
});
}
if (useMassTransit)
{
alterations.UseMassTransitDispatcher();
}
})
.UseWorkflowContexts();
if (useQuartz)
{
elsa.UseQuartz(quartz => { quartz.UseSqlite(sqliteConnectionString); });
}
if (useSignalR)
{
elsa.UseRealTimeWorkflows();
}
if (useAzureServiceBus)
elsa.UseAzureServiceBus(asb => asb.AzureServiceBusOptions += options => configuration.GetSection("AzureServiceBus").Bind(options));
if (useMassTransit)
{
elsa.UseMassTransit(massTransit =>
{
massTransit.DisableConsumers = appRole == ApplicationRole.Api;
if (massTransitBroker == MassTransitBroker.AzureServiceBus)
{
massTransit.UseAzureServiceBus(azureServiceBusConnectionString, serviceBusFeature => serviceBusFeature.ConfigureServiceBus = bus =>
{
bus.PrefetchCount = 50;
bus.LockDuration = TimeSpan.FromMinutes(5);
bus.MaxConcurrentCalls = 32;
bus.MaxDeliveryCount = 8;
// etc.
});
}
if (massTransitBroker == MassTransitBroker.RabbitMq)
{
massTransit.UseRabbitMq(rabbitMqConnectionString, rabbit => rabbit.ConfigureServiceBus = bus =>
{
bus.PrefetchCount = 50;
bus.Durable = true;
bus.AutoDelete = false;
bus.ConcurrentMessageLimit = 32;
// etc.
});
}
});
}
if (distributedCachingTransport != DistributedCachingTransport.None)
{
elsa.UseDistributedCache(distributedCaching =>
{
if (distributedCachingTransport == DistributedCachingTransport.MassTransit) distributedCaching.UseMassTransit();
if (distributedCachingTransport == DistributedCachingTransport.ProtoActor) distributedCaching.UseProtoActor();
});
}
if (useAzureServiceBusModule)
{
elsa.UseAzureServiceBus(azureServiceBusConnectionString, asb =>
{
asb.AzureServiceBusOptions = options => configuration.GetSection("AzureServiceBus").Bind(options);
});
}
if (useAgents)
{
elsa
.UseAgentActivities()
.UseAgentPersistence(persistence => persistence.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseAgentsApi()
;
services.Configure<AgentsOptions>(options => builder.Configuration.GetSection("Agents").Bind(options));
}
if (useSecrets)
{
elsa
.UseSecrets()
.UseSecretsManagement(management =>
{
management.ConfigureOptions(options => configuration.GetSection("Secrets:Management").Bind(options));
management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString));
})
.UseSecretsApi()
.UseSecretsScripting()
;
}
if (useMultitenancy)
{
elsa.UseTenants(tenants =>
{
tenants.TenantsOptions = options =>
{
configuration.GetSection("Multitenancy").Bind(options);
options.TenantResolverPipelineBuilder
.Append<ClaimsTenantResolver>();
};
tenants.UseConfigurationBasedTenantsProvider();
});
}
elsa.InstallDropIns(options => options.DropInRootDirectory = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "DropIns"));
elsa.AddSwagger();
elsa.AddFastEndpointsAssembly<Program>();
ConfigureForTest?.Invoke(elsa);
});
// Obfuscate HTTP request headers.
services.AddActivityStateFilter<HttpRequestAuthenticationHeaderFilter>();
//services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
services.AddHealthChecks();
services.AddControllers();
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*")));
// Build the web application.
var app = builder.Build();
// Configure the pipeline.
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
// CORS.
app.UseCors();
// Health checks.
app.MapHealthChecks("/");
// Routing used for SignalR.
app.UseRouting();
// Security.
app.UseAuthentication();
app.UseAuthorization();
// Multitenancy.
if(useMultitenancy)
app.UseTenants();
// Elsa API endpoints for designer.
var routePrefix = app.Services.GetRequiredService<IOptions<ApiEndpointOptions>>().Value.RoutePrefix;
app.UseWorkflowsApi(routePrefix);
// Captures unhandled exceptions and returns a JSON response.
app.UseJsonSerializationErrorHandler();
// Elsa HTTP Endpoint activities.
app.UseWorkflows();
app.MapControllers();
// Swagger API documentation.
if (app.Environment.IsDevelopment())
{
app.UseSwaggerUI();
}
// SignalR.
if (useSignalR)
{
app.UseWorkflowsSignalRHubs();
}
// Run.
await app.RunAsync();
/// The main entry point for the application made public for end to end testing.
[UsedImplicitly]
public partial class Program
{
/// Set by the test runner to configure the module for testing.
public static Action<IModule>? ConfigureForTest { get; set; }
}