using Microsoft.AspNetCore.Authentication.Negotiate; using Microsoft.AspNetCore.Authorization; using SQLInsight.Repository; using SQLInsight.Web; using SQLInsight.Web.Api; using SQLInsight.Web.Components; if (args.FirstOrDefault() == "new-api-token") { Console.OutputEncoding = System.Text.Encoding.UTF8; var (token, hash) = ApiTokenHandler.Create(); var name = args.ElementAtOrDefault(1) ?? "prometheus"; Console.WriteLine($"Token (vises kun nu, gem det i det system, der skal hente data): {token}"); Console.WriteLine($"Tilføj i appsettings.json under SQLInsight:Web:ApiTokens: \"{name}\": \"{hash}\""); return; } var builder = WebApplication.CreateBuilder(args); InstallerSettings.Add(builder.Configuration, AppContext.BaseDirectory); // Runs as a Windows service with its own Kestrel web server, or behind IIS (ASP.NET Core Module), or // as a systemd service on Linux. Each call is a no-op when not hosted that way. builder.Services.AddWindowsService(o => o.ServiceName = "SQLInsight Web"); builder.Services.AddSystemd(); builder.Services.Configure(builder.Configuration.GetSection(WebOptions.SectionName)); var web = builder.Configuration.GetSection(WebOptions.SectionName).Get() ?? new WebOptions(); var connectionString = builder.Configuration["SQLInsight:Repository:ConnectionString"]; if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException("SQLInsight:Repository:ConnectionString mangler. Angiv forbindelsen til repository-databasen."); } builder.Services.AddSingleton(new InsightRepository(connectionString)); builder.Services.AddSingleton(); // Danish text (æ, ø, å) as characters rather than æ entities. HTML-significant characters are still encoded. builder.Services.AddSingleton(System.Text.Encodings.Web.HtmlEncoder.Create(System.Text.Unicode.UnicodeRanges.All)); builder.Services.AddRazorComponents(); var windowsAuth = string.Equals(web.Authentication, "Windows", StringComparison.OrdinalIgnoreCase); if (windowsAuth) { // Browsers log in with Windows authentication; machines calling /api/ or /metrics may use a bearer token instead. builder.Services.AddAuthentication("Smart") .AddPolicyScheme("Smart", "Windows eller API-token", o => o.ForwardDefaultSelector = context => ApiTokenHandler.AppliesTo(context.Request.Path) && context.Request.Headers.Authorization.ToString().StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) ? ApiTokenHandler.SchemeName : NegotiateDefaults.AuthenticationScheme) .AddNegotiate() .AddScheme(ApiTokenHandler.SchemeName, null); builder.Services.AddAuthorization(options => { var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser(); if (web.AllowedGroups.Count > 0) { policy.RequireAssertion(ctx => ctx.User.HasClaim(c => c.Type == ApiTokenHandler.ClaimType) || web.AllowedGroups.Any(ctx.User.IsInRole)); } options.FallbackPolicy = policy.Build(); }); } else if (!builder.Environment.IsDevelopment()) { throw new InvalidOperationException("SQLInsight:Web:Authentication = None er kun tilladt i Development. Web-UI'et viser oplysninger om jeres SQL Servere."); } var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/fejl", createScopeForErrors: true); app.UseHsts(); } app.Use(async (context, next) => { context.Response.Headers.XContentTypeOptions = "nosniff"; context.Response.Headers.XFrameOptions = "DENY"; context.Response.Headers["Referrer-Policy"] = "no-referrer"; context.Response.Headers.ContentSecurityPolicy = "default-src 'self'; style-src 'self'; script-src 'self'; img-src 'self' data:"; await next(); }); if (windowsAuth) { app.UseAuthentication(); app.UseAuthorization(); } app.UseAntiforgery(); app.MapStaticAssets(); app.MapGet("/health", () => Results.Ok(new { status = "ok" })).AllowAnonymous(); // Deadlock graph as .xdl, which SSMS opens as a graphical deadlock graph. app.MapGet("/deadlocks/{id:long}.xdl", async (long id, InsightRepository repository, CancellationToken ct) => await repository.GetDeadlockAsync(id, ct) is { } deadlock ? Results.File(System.Text.Encoding.UTF8.GetBytes(deadlock.GraphXml), "application/xml", $"deadlock-{id}.xdl") : Results.NotFound()); IntegrationEndpoints.Map(app); Support.Map(app); // Management and compliance report as PDF, for the last N days. app.MapGet("/rapporter/driftsrapport.pdf", async (int? dage, InsightRepository repository, Microsoft.Extensions.Options.IOptions options, CancellationToken ct) => { var days = Math.Clamp(dage ?? 30, 1, 366); var settings = options.Value; var zone = settings.TimeZone is { Length: > 0 } id ? TimeZoneInfo.FindSystemTimeZoneById(id) : TimeZoneInfo.Local; var now = DateTimeOffset.UtcNow; var data = await SQLInsight.Reports.ReportData.LoadAsync(repository, settings.Organization, now.AddDays(-days), now, zone, TimeProvider.System, ct); return Results.File(SQLInsight.Reports.ManagementReport.Render(data), "application/pdf", $"driftsrapport-{now:yyyy-MM-dd}.pdf"); }); // Time window analysis as PDF. Contains statement texts: produced here, for the person who is logged in. app.MapGet("/analyse.pdf", async (string? instans, string? fra, string? til, string? sortering, InsightRepository repository, Display display, Microsoft.Extensions.Options.IOptions options, CancellationToken ct) => { if (instans is null || await repository.GetInstanceAsync(instans, ct) is not { } instance) { return Results.NotFound(); } if (!SQLInsight.Reports.TimeWindow.TryParse(fra, til, display.Zone, out var from, out var to, out var error)) { return Results.BadRequest(error); } var now = DateTimeOffset.UtcNow; var analysis = await SQLInsight.Reports.TimeWindowAnalysis.LoadAsync(repository, instance, from, to, now, new RetentionOptions().RawDays, SQLInsight.Reports.AnalysisOrder.Parse(sortering), ct); var file = $"analyse-{string.Concat(instance.Name.Select(c => char.IsLetterOrDigit(c) || c == '-' ? c : '_'))}-{SQLInsight.Reports.TimeWindow.ToInput(from, display.Zone).Replace(':', '-')}.pdf"; return Results.File(SQLInsight.Reports.TimeWindowReport.Render(analysis, options.Value.Organization, display.Zone), "application/pdf", file); }); app.MapRazorComponents(); // The web UI only reads. The agent owns the schema and migrates it, so the UI's login needs no more than db_datareader. app.Run(); /// Entry point, visible to integration tests. public partial class Program;