Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Globalization;
using System.IO.Compression;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -81,6 +82,23 @@
else
group.RequireAuthorization(options.AuthorizationPolicy);
}
else if (endpoints.ServiceProvider.GetService<IAuthenticationSchemeProvider>() is null)
{
// Auth inherits from the host (ADR D20) — the engine does not impose a default. But when
// no authentication is configured on the host at all AND RequireAuthorization was not set,
// this management surface (trigger runs, register reports, store source connection
// strings, mutate schedules, download artifacts) is reachable unauthenticated. That is a
// valid deployment only behind a trusted boundary; warn once at startup so it is a
// deliberate choice, not a silent default.
endpoints.ServiceProvider.GetService<ILoggerFactory>()?
.CreateLogger("NeoReports.AspNetCore")
.LogWarning(
"NeoReports endpoints mapped at '{Prefix}' with no authentication configured on the host and " +
"NeoReportsEndpointOptions.RequireAuthorization not set — the report management API is reachable " +
"unauthenticated. Configure the host's authentication/authorization (or set RequireAuthorization) " +
"before exposing it beyond a trusted network.",
prefix);
}

// Compiling a report — Create and Validate below — must resolve IConfigSourceProvider
// (and, for a Ref-based source, ISourceRegistry) through the app's ROOT provider, never
Expand Down Expand Up @@ -128,7 +146,7 @@
return group;
}

private static async Task<IResult> RunReportAsync(

Check warning on line 149 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 149 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 149 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Method has 9 parameters, which is greater than the 7 authorized.

Check warning on line 149 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Method has 9 parameters, which is greater than the 7 authorized.
string name,
string? mode,
RunReportRequest? body,
Expand Down Expand Up @@ -1034,7 +1052,7 @@

try
{
SchemaCatalog catalog = await explorer!.GetCatalogAsync(definition!, cancellationToken).ConfigureAwait(false);

Check warning on line 1055 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1055 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1055 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1055 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

See more on https://sonarcloud.io/project/issues?id=lugarini_NeoReports&issues=AZ-zQR6KxvXlsfYEU4Fc&open=AZ-zQR6KxvXlsfYEU4Fc&pullRequest=227
return Results.Ok(ToCatalogResponse(catalog));
}
catch (Exception ex) when (ex is not OperationCanceledException)
Expand All @@ -1056,7 +1074,7 @@

try
{
TablePreview preview = await explorer!

Check warning on line 1077 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1077 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1077 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View workflow job for this annotation

GitHub Actions / SonarCloud

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

Check warning on line 1077 in src/Integrations/NeoReports.AspNetCore/NeoReportsEndpointRouteBuilderExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this null-forgiving operator; the compiler already knows this expression is not null here.

See more on https://sonarcloud.io/project/issues?id=lugarini_NeoReports&issues=AZ-zQR6KxvXlsfYEU4Fd&open=AZ-zQR6KxvXlsfYEU4Fd&pullRequest=227
.PreviewTableAsync(definition!, schema ?? string.Empty, table, SchemaPreviewTop, cancellationToken)
.ConfigureAwait(false);
return Results.Ok(new TablePreviewResponse(preview.Columns, preview.Rows));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NeoReports.AspNetCore;
using NeoReports.Core.Building;
using NeoReports.Core.DependencyInjection;
using Shouldly;
using Xunit;
using static NeoReports.Core.Building.ReportColumns;
using static NeoReports.Formats.Csv.Format;

namespace NeoReports.AspNetCore.IntegrationTests;

public class AuthWarningTests
{
private static async Task<IReadOnlyCollection<string>> WarningsFromMapping(
Action<IServiceCollection>? extraServices, Action<NeoReportsEndpointOptions>? options)
{
var capture = new CapturingLoggerProvider();
using var host = await new HostBuilder()
.ConfigureWebHost(web =>
{
web.UseTestServer();
web.ConfigureServices(services =>
{
services.AddRouting();
services.AddLogging(b => b.AddProvider(capture));
services.AddReport<Sale>("sales", b => b
.From(new InMemorySource(rows: 1, pageSize: 10))
.Column(v => v.Id, "ID")
.To(Csv(o => o.Delimiter(';'))));
extraServices?.Invoke(services);
})
.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(e => e.MapNeoReports("/api", options));
});
})
.StartAsync();

return capture.Warnings;
}

[Fact]
public async Task Warns_when_mapped_without_auth_and_no_authentication_configured()
{
var warnings = await WarningsFromMapping(extraServices: null, options: null);
warnings.ShouldContain(w => w.Contains("reachable", StringComparison.Ordinal) && w.Contains("/api", StringComparison.Ordinal));
}

[Fact]
public async Task Does_not_warn_when_the_host_has_authentication_configured()
{
var warnings = await WarningsFromMapping(
extraServices: s => s.AddAuthentication(), options: null);
warnings.ShouldNotContain(w => w.Contains("reachable unauthenticated", StringComparison.Ordinal));
}

[Fact]
public async Task Does_not_warn_when_authorization_is_required()
{
var warnings = await WarningsFromMapping(
extraServices: s => s.AddAuthorizationBuilder(),
options: o => o.RequireAuthorization = true);
warnings.ShouldNotContain(w => w.Contains("reachable unauthenticated", StringComparison.Ordinal));
}

private sealed class CapturingLoggerProvider : ILoggerProvider
{
public ConcurrentBag<string> Warnings { get; } = [];

public ILogger CreateLogger(string categoryName) => new CapturingLogger(Warnings);

public void Dispose() { }

private sealed class CapturingLogger(ConcurrentBag<string> warnings) : ILogger
{
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull => null;

public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Warning;

public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (logLevel >= LogLevel.Warning)
warnings.Add(formatter(state, exception));
}
}
}
}