* Extract dashboard contributors into modules * Install dashboard widgets by default * Enable dashboard features in modular server config
313 lines
12 KiB
C#
313 lines
12 KiB
C#
using System.Text.Encodings.Web;
|
|
using System.Threading.RateLimiting;
|
|
using Elsa.Caching.Options;
|
|
using Elsa.Common.RecurringTasks;
|
|
using Elsa.Expressions.Helpers;
|
|
using Elsa.Extensions;
|
|
using Elsa.Features.Services;
|
|
using Elsa.Http.Options;
|
|
using Elsa.Identity.Multitenancy;
|
|
using Elsa.Persistence.EFCore.Extensions;
|
|
using Elsa.Persistence.EFCore.Modules.Management;
|
|
using Elsa.Persistence.EFCore.Modules.Runtime;
|
|
using Elsa.Server.Web.Activities;
|
|
using Elsa.Server.Web.ActivityHosts;
|
|
using Elsa.Server.Web.Filters;
|
|
using Elsa.Tenants;
|
|
using Elsa.Tenants.AspNetCore;
|
|
using Elsa.Tenants.Extensions;
|
|
using Elsa.WorkflowProviders.BlobStorage.ElsaScript.Extensions;
|
|
using Elsa.Workflows;
|
|
using Elsa.Workflows.Activities.Flowchart.Extensions;
|
|
using Elsa.Workflows.Api;
|
|
using Elsa.Workflows.CommitStates.Strategies;
|
|
using Elsa.Workflows.IncidentStrategies;
|
|
using Elsa.Workflows.LogPersistence;
|
|
using Elsa.Workflows.Options;
|
|
using Elsa.Workflows.Runtime.Distributed.Extensions;
|
|
using Elsa.Workflows.Runtime.Options;
|
|
using Elsa.Workflows.Runtime.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Microsoft.AspNetCore.RateLimiting;
|
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
// ReSharper disable RedundantAssignment
|
|
const bool useReadOnlyMode = false;
|
|
const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated requests.
|
|
const bool useStructuredLogs = false; // Enable to inspect backend logs from Elsa Studio.
|
|
const bool useMultitenancy = true;
|
|
const bool disableVariableWrappers = false;
|
|
const string elsaApiRateLimitingPolicy = "elsa-api";
|
|
const string httpWorkflowRateLimitingPolicy = "elsa-http-workflows";
|
|
|
|
ObjectConverter.StrictMode = true;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
var services = builder.Services;
|
|
var configuration = builder.Configuration;
|
|
var identitySection = configuration.GetSection("Identity");
|
|
var identityTokenSection = identitySection.GetSection("Tokens");
|
|
var ingressRateLimitingSection = configuration.GetSection("IngressRateLimiting");
|
|
var useIngressRateLimiting = ingressRateLimitingSection.GetValue("Enabled", false);
|
|
var registerIngressRateLimitingPolicies = ingressRateLimitingSection.GetValue("RegisterReferencePolicies", useIngressRateLimiting);
|
|
var configuredAllowLocalDistributedRuntimeLockProvider = configuration.GetValue<bool?>("DistributedRuntime:AllowLocalLockProviderInDistributedRuntime");
|
|
var allowLocalDistributedRuntimeLockProvider =
|
|
configuredAllowLocalDistributedRuntimeLockProvider ?? builder.Environment.IsDevelopment();
|
|
|
|
services
|
|
.AddElsa(elsa =>
|
|
{
|
|
elsa
|
|
.AddActivitiesFrom<Program>()
|
|
.AddActivityHost<Penguin>()
|
|
.AddWorkflowsFrom<Program>()
|
|
.UseIdentity(identity =>
|
|
{
|
|
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.UseCommitStrategies(strategies =>
|
|
{
|
|
strategies.AddStandardStrategies();
|
|
strategies.Add("Every 10 seconds", new PeriodicWorkflowStrategy(TimeSpan.FromSeconds(10)));
|
|
});
|
|
})
|
|
.UseFlowchart(flowchart => flowchart.UseTokenBasedExecution())
|
|
.UseWorkflowManagement(management =>
|
|
{
|
|
management.UseEntityFrameworkCore(ef => ef.UseSqlite());
|
|
management.SetDefaultLogPersistenceMode(LogPersistenceMode.Inherit);
|
|
management.UseCache();
|
|
management.UseReadOnlyMode(useReadOnlyMode);
|
|
})
|
|
.UseWorkflowRuntime(runtime =>
|
|
{
|
|
runtime.UseEntityFrameworkCore(ef => ef.UseSqlite());
|
|
runtime.UseCache();
|
|
runtime.UseDistributedRuntime();
|
|
// This sample host acknowledges single-host file-system locks in development or explicit single-host opt-in.
|
|
runtime.DistributedLockingOptions = options => options.AllowLocalLockProviderInDistributedRuntime = allowLocalDistributedRuntimeLockProvider;
|
|
})
|
|
.UseWorkflowsApi()
|
|
.UseDashboardApi()
|
|
.UseWorkflowRuntimeDashboard()
|
|
.UseFluentStorageProvider()
|
|
.UseElsaScriptBlobStorage()
|
|
.UseScheduling()
|
|
.UseCSharp(options =>
|
|
{
|
|
configuration.GetSection("Scripting:CSharp").Bind(options);
|
|
options.DisableWrappers = disableVariableWrappers;
|
|
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);
|
|
|
|
options.AddScript(sb =>
|
|
{
|
|
sb.AppendLine("def greet():");
|
|
sb.AppendLine(" return \"Hello, welcome to Python!\"");
|
|
});
|
|
};
|
|
})
|
|
.UseLiquid(liquid => liquid.FluidOptions = options => options.Encoder = HtmlEncoder.Default)
|
|
.UseHttp(http =>
|
|
{
|
|
http.ConfigureHttpOptions = options => configuration.GetSection("Http").Bind(options);
|
|
http.UseCache();
|
|
});
|
|
|
|
if(useMultitenancy)
|
|
{
|
|
elsa.UseTenants(tenants =>
|
|
{
|
|
tenants.UseConfigurationBasedTenantsProvider(options => configuration.GetSection("Multitenancy").Bind(options));
|
|
tenants.ConfigureMultitenancy(options => options.TenantResolverPipelineBuilder = new TenantResolverPipelineBuilder()
|
|
.Append<CurrentUserTenantResolver>());
|
|
});
|
|
}
|
|
|
|
if(useStructuredLogs)
|
|
{
|
|
elsa
|
|
.UseStructuredLogs()
|
|
.UseStructuredLogsDashboard();
|
|
}
|
|
|
|
ConfigureForTest?.Invoke(elsa);
|
|
});
|
|
|
|
// Obfuscate HTTP request headers.
|
|
services.AddActivityStateFilter<HttpRequestAuthenticationHeaderFilter>();
|
|
|
|
// Optionally configure recurring tasks using alternative schedules.
|
|
services.Configure<RecurringTaskOptions>(options =>
|
|
{
|
|
options.Schedule.ConfigureTask<TriggerBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
|
|
options.Schedule.ConfigureTask<PurgeBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
|
|
options.Schedule.ConfigureTask<RestartInterruptedWorkflowsTask>(TimeSpan.FromSeconds(15));
|
|
});
|
|
|
|
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
|
|
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(3600));
|
|
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
|
|
services.Configure<IncidentOptions>(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
|
|
if (useIngressRateLimiting)
|
|
{
|
|
services.PostConfigure<ApiEndpointOptions>(options =>
|
|
{
|
|
if (options.RateLimitingPolicyName == null)
|
|
options.RateLimitingPolicyName = elsaApiRateLimitingPolicy;
|
|
});
|
|
services.PostConfigure<HttpActivityOptions>(options =>
|
|
{
|
|
if (options.RateLimitingPolicyName == null)
|
|
options.RateLimitingPolicyName = httpWorkflowRateLimitingPolicy;
|
|
});
|
|
}
|
|
|
|
services.AddRateLimiter(options =>
|
|
{
|
|
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
|
|
if (registerIngressRateLimitingPolicies)
|
|
{
|
|
options.AddFixedWindowLimiter(elsaApiRateLimitingPolicy, limiterOptions =>
|
|
{
|
|
limiterOptions.PermitLimit = ingressRateLimitingSection.GetValue("ApiPermitLimit", 120);
|
|
limiterOptions.Window = TimeSpan.FromSeconds(ingressRateLimitingSection.GetValue("ApiWindowSeconds", 60));
|
|
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
limiterOptions.QueueLimit = ingressRateLimitingSection.GetValue("ApiQueueLimit", 0);
|
|
});
|
|
options.AddFixedWindowLimiter(httpWorkflowRateLimitingPolicy, limiterOptions =>
|
|
{
|
|
limiterOptions.PermitLimit = ingressRateLimitingSection.GetValue("HttpWorkflowPermitLimit", 60);
|
|
limiterOptions.Window = TimeSpan.FromSeconds(ingressRateLimitingSection.GetValue("HttpWorkflowWindowSeconds", 60));
|
|
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
|
|
limiterOptions.QueueLimit = ingressRateLimitingSection.GetValue("HttpWorkflowQueueLimit", 0);
|
|
});
|
|
}
|
|
});
|
|
services
|
|
.AddHealthChecks()
|
|
.AddElsaReadinessChecks(includeDistributedLocks: true);
|
|
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("/health/live", new()
|
|
{
|
|
Predicate = _ => false
|
|
});
|
|
app.MapHealthChecks("/health/ready", new()
|
|
{
|
|
Predicate = check => check.Tags.Contains(HealthCheckExtensions.ElsaTag) && check.Tags.Contains(HealthCheckExtensions.ReadinessTag),
|
|
ResultStatusCodes =
|
|
{
|
|
[HealthStatus.Degraded] = StatusCodes.Status503ServiceUnavailable,
|
|
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable
|
|
}
|
|
});
|
|
app.MapHealthChecks("/", new()
|
|
{
|
|
Predicate = _ => false
|
|
});
|
|
|
|
// Elsa API endpoints for designer.
|
|
var apiEndpointOptions = app.Services.GetRequiredService<IOptions<ApiEndpointOptions>>().Value;
|
|
var routePrefix = apiEndpointOptions.RoutePrefix;
|
|
app.MapWorkflowsApi(routePrefix);
|
|
|
|
// Routing used for SignalR.
|
|
app.UseRouting();
|
|
|
|
app.UseWorkflowsApiRateLimiting(routePrefix, apiEndpointOptions.RateLimitingPolicyName);
|
|
|
|
// Elsa HTTP Endpoint activities.
|
|
var httpActivityOptions = app.Services.GetRequiredService<IOptions<HttpActivityOptions>>().Value;
|
|
app.UseWorkflowsRateLimiting(httpActivityOptions.BasePath, httpActivityOptions.RateLimitingPolicyName);
|
|
if (useIngressRateLimiting ||
|
|
!string.IsNullOrWhiteSpace(apiEndpointOptions.RateLimitingPolicyName) ||
|
|
!string.IsNullOrWhiteSpace(httpActivityOptions.RateLimitingPolicyName))
|
|
app.UseRateLimiter();
|
|
|
|
// Security.
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
// Multitenancy.
|
|
if (useMultitenancy)
|
|
app.UseTenants();
|
|
|
|
// Captures unhandled exceptions and returns a JSON response.
|
|
app.UseJsonSerializationErrorHandler();
|
|
|
|
app.UseWorkflows();
|
|
|
|
app.MapControllers();
|
|
|
|
// Swagger API documentation.
|
|
if (app.Environment.IsDevelopment())
|
|
{
|
|
app.UseSwaggerUI();
|
|
}
|
|
|
|
// SignalR.
|
|
if (useSignalR)
|
|
{
|
|
app.UseWorkflowsSignalRHubs();
|
|
}
|
|
|
|
// Structured log streaming for Studio diagnostics.
|
|
if (useStructuredLogs)
|
|
{
|
|
app.UseStructuredLogs();
|
|
}
|
|
|
|
// Run.
|
|
await app.RunAsync();
|
|
|
|
/// <summary>
|
|
/// The main entry point for the application made public for end to end testing.
|
|
/// </summary>
|
|
[UsedImplicitly]
|
|
public partial class Program
|
|
{
|
|
/// <summary>
|
|
/// Set by the test runner to configure the module for testing.
|
|
/// </summary>
|
|
public static Action<IModule>? ConfigureForTest { get; set; }
|
|
}
|