mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Merge pull request #2278 from DuendeSoftware/pg/bff-licensing
Log out number of Frontends
This commit is contained in:
commit
b35a7fd236
31 changed files with 1367 additions and 858 deletions
|
|
@ -28,7 +28,7 @@ namespace Duende.Bff.Blazor;
|
|||
/// <summary>
|
||||
/// This is a server-side AuthenticationStateProvider that uses
|
||||
/// PersistentComponentState to flow the authentication state to the client which
|
||||
/// is then used to initialize the authentication state in the WASM application.
|
||||
/// is then used to initialize the authentication state in the WASM application.
|
||||
/// </summary>
|
||||
internal sealed class BffServerAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider, IDisposable
|
||||
{
|
||||
|
|
@ -68,16 +68,7 @@ internal sealed class BffServerAuthenticationStateProvider : RevalidatingServerA
|
|||
AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||
_subscription = _state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
|
||||
|
||||
CheckLicense(licenseValidator);
|
||||
}
|
||||
|
||||
|
||||
internal static void CheckLicense(LicenseValidator validator)
|
||||
{
|
||||
if (!validator.IsValid())
|
||||
{
|
||||
// todo: license enforcement
|
||||
}
|
||||
licenseValidator.CheckLicense();
|
||||
}
|
||||
|
||||
private void OnAuthenticationStateChanged(Task<AuthenticationState> task) => _authenticationStateTask = task;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
<AssemblyName>Duende.BFF</AssemblyName>
|
||||
<Description>Backend for frontend (BFF) host for ASP.NET Core</Description>
|
||||
<!-- Related to https://github.com/dotnet/sdk/issues/50676-->
|
||||
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>-->
|
||||
<!-- <CompilerGeneratedFilesOutputPath>Otel/Generated</CompilerGeneratedFilesOutputPath>-->
|
||||
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>-->
|
||||
<!-- <CompilerGeneratedFilesOutputPath>Otel/Generated</CompilerGeneratedFilesOutputPath>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -25,8 +25,4 @@
|
|||
<InternalsVisibleTo Include="Duende.Bff.Yarp"/>
|
||||
<InternalsVisibleTo Include="Duende.Bff.Tests"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Duende.Private.Licensing"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ using Duende.AccessTokenManagement.OpenIdConnect;
|
|||
using Duende.Bff.AccessTokenManagement;
|
||||
using Duende.Bff.Builder;
|
||||
using Duende.Bff.Configuration;
|
||||
using Duende.Bff.Diagnostics;
|
||||
using Duende.Bff.Diagnostics.DiagnosticEntries;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Duende.Bff.DynamicFrontends.Internal;
|
||||
using Duende.Bff.Endpoints;
|
||||
|
|
@ -16,7 +18,6 @@ using Duende.Bff.SessionManagement.Configuration;
|
|||
using Duende.Bff.SessionManagement.Revocation;
|
||||
using Duende.Bff.SessionManagement.SessionStore;
|
||||
using Duende.Bff.SessionManagement.TicketStore;
|
||||
using Duende.Private.Licensing;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
|
|
@ -46,9 +47,14 @@ public static class BffBuilderExtensions
|
|||
|
||||
internal static T AddBaseBffServices<T>(this T builder) where T : IBffServicesBuilder
|
||||
{
|
||||
builder.Services.AddSingleton<GetLicenseKey>(sp => () => sp.GetRequiredService<IOptions<BffOptions>>().Value.LicenseKey);
|
||||
builder.Services.AddSingleton<LicenseAccessor<BffLicense>>();
|
||||
builder.Services.AddSingleton<BffLicense>(sp => sp.GetRequiredService<LicenseAccessor<BffLicense>>().Current);
|
||||
builder.Services.AddSingleton<GetLicenseKey>(sp =>
|
||||
() => sp.GetRequiredService<IOptions<BffOptions>>().Value.LicenseKey);
|
||||
builder.Services.AddSingleton<License>(sp =>
|
||||
{
|
||||
var accessor = sp.GetRequiredService<LicenseAccessor>();
|
||||
return accessor.Current;
|
||||
});
|
||||
builder.Services.AddSingleton<LicenseAccessor>();
|
||||
builder.Services.TryAddSingleton<LicenseValidator>();
|
||||
builder.Services.AddDistributedMemoryCache();
|
||||
|
||||
|
|
@ -59,7 +65,8 @@ public static class BffBuilderExtensions
|
|||
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, BffConfigureOpenIdConnectOptions>();
|
||||
builder.Services.AddOpenIdConnectAccessTokenManagement();
|
||||
|
||||
builder.Services.AddSingleton<IConfigureOptions<UserTokenManagementOptions>, ConfigureUserTokenManagementOptions>();
|
||||
builder.Services
|
||||
.AddSingleton<IConfigureOptions<UserTokenManagementOptions>, ConfigureUserTokenManagementOptions>();
|
||||
|
||||
builder.Services.AddTransient<IReturnUrlValidator, LocalUrlReturnUrlValidator>();
|
||||
builder.Services.TryAddSingleton<IAccessTokenRetriever, DefaultAccessTokenRetriever>();
|
||||
|
|
@ -79,12 +86,16 @@ public static class BffBuilderExtensions
|
|||
builder.Services.TryAddTransient<ISessionRevocationService, NopSessionRevocationService>();
|
||||
|
||||
// cookie configuration
|
||||
builder.Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureSlidingExpirationCheck>();
|
||||
builder.Services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureApplicationCookieRevokeRefreshToken>();
|
||||
builder.Services
|
||||
.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureSlidingExpirationCheck>();
|
||||
builder.Services
|
||||
.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>,
|
||||
PostConfigureApplicationCookieRevokeRefreshToken>();
|
||||
builder.Services.AddSingleton<ActiveCookieAuthenticationScheme>();
|
||||
builder.Services.AddSingleton<ActiveOpenIdConnectAuthenticationScheme>();
|
||||
|
||||
builder.Services.AddSingleton<IPostConfigureOptions<OpenIdConnectOptions>, PostConfigureOidcOptionsForSilentLogin>();
|
||||
builder.Services
|
||||
.AddSingleton<IPostConfigureOptions<OpenIdConnectOptions>, PostConfigureOidcOptionsForSilentLogin>();
|
||||
|
||||
AddBffMetrics(builder);
|
||||
|
||||
|
|
@ -94,14 +105,30 @@ public static class BffBuilderExtensions
|
|||
|
||||
// Make sure the session partitioning is registered. There are a few codepaths that require this injected
|
||||
// even if you are not using session management.
|
||||
builder.Services.AddSingleton<BuildUserSessionPartitionKey>(sp => sp.GetRequiredService<UserSessionPartitionKeyBuilder>().BuildPartitionKey);
|
||||
builder.Services.AddSingleton<BuildUserSessionPartitionKey>(sp =>
|
||||
sp.GetRequiredService<UserSessionPartitionKeyBuilder>().BuildPartitionKey);
|
||||
builder.Services.AddSingleton<UserSessionPartitionKeyBuilder>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
internal static void AddBffMetrics<T>(T builder) where T : IBffBuilder => builder.Services.AddSingleton<BffMetrics>();
|
||||
internal static void AddBffMetrics<T>(T builder) where T : IBffBuilder =>
|
||||
builder.Services.AddSingleton<BffMetrics>();
|
||||
|
||||
internal static T AddDiagnostics<T>(this T builder)
|
||||
where T : IBffServicesBuilder
|
||||
{
|
||||
builder.Services.AddSingleton<IDiagnosticEntry, BasicServerInfoDiagnosticEntry>();
|
||||
builder.Services.AddSingleton<IDiagnosticEntry, AssemblyInfoDiagnosticEntry>();
|
||||
builder.Services.AddSingleton<IDiagnosticEntry, FrontendCountDiagnosticEntry>();
|
||||
builder.Services.AddSingleton<DiagnosticSummary>();
|
||||
builder.Services.AddSingleton(serviceProvider => new DiagnosticDataService(
|
||||
serviceProvider.GetRequiredService<TimeProvider>().GetUtcNow().UtcDateTime,
|
||||
serviceProvider.GetServices<IDiagnosticEntry>()));
|
||||
builder.Services.AddHostedService<DiagnosticHostedService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
internal static T AddDynamicFrontends<T>(this T builder)
|
||||
where T : IBffServicesBuilder
|
||||
|
|
@ -125,7 +152,8 @@ public static class BffBuilderExtensions
|
|||
|
||||
// Configure the AspNet Core Authentication settings if no
|
||||
// .AddAuthentication().AddCookie().AddOpenIdConnect() was added
|
||||
builder.Services.AddSingleton<IPostConfigureOptions<AuthenticationOptions>, BffConfigureAuthenticationOptions>();
|
||||
builder.Services
|
||||
.AddSingleton<IPostConfigureOptions<AuthenticationOptions>, BffConfigureAuthenticationOptions>();
|
||||
|
||||
builder.Services.AddSingleton<IConfigureOptions<CookieAuthenticationOptions>, BffConfigureCookieOptions>();
|
||||
|
||||
|
|
@ -133,8 +161,10 @@ public static class BffBuilderExtensions
|
|||
|
||||
// Add 'default' configure methods that would have been added by
|
||||
// .AddAuthentication().AddCookie().AddOpenIdConnect()
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor
|
||||
.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
|
||||
|
||||
builder.Services.TryAddSingleton<IStaticFilesClient, StaticFilesHttpClient>();
|
||||
|
||||
|
|
@ -180,11 +210,14 @@ public static class BffBuilderExtensions
|
|||
|
||||
internal static void AddServerSideSessionsSupportingServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<BuildUserSessionPartitionKey>(sp => sp.GetRequiredService<UserSessionPartitionKeyBuilder>().BuildPartitionKey);
|
||||
services.AddSingleton<BuildUserSessionPartitionKey>(sp =>
|
||||
sp.GetRequiredService<UserSessionPartitionKeyBuilder>().BuildPartitionKey);
|
||||
services.AddSingleton<UserSessionPartitionKeyBuilder>();
|
||||
|
||||
services.AddSingleton<UserSessionPartitionKeyBuilder>();
|
||||
services.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureApplicationCookieTicketStore>();
|
||||
services
|
||||
.AddSingleton<IPostConfigureOptions<CookieAuthenticationOptions>,
|
||||
PostConfigureApplicationCookieTicketStore>();
|
||||
services.AddTransient<IServerTicketStore, ServerSideTicketStore>();
|
||||
services.AddTransient<ISessionRevocationService, SessionRevocationService>();
|
||||
// only add if not already in DI
|
||||
|
|
|
|||
|
|
@ -191,22 +191,16 @@ public static class BffEndpointRouteBuilderExtensions
|
|||
internal static bool AlreadyMappedManagementEndpoint(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
PathString route) => endpoints.DataSources.Any(ds =>
|
||||
ds.Endpoints
|
||||
.OfType<RouteEndpoint>()
|
||||
.Any(e =>
|
||||
e.RoutePattern.RawText == route.Value));
|
||||
ds.Endpoints
|
||||
.OfType<RouteEndpoint>()
|
||||
.Any(e =>
|
||||
e.RoutePattern.RawText == route.Value));
|
||||
|
||||
internal static void CheckLicense(this IEndpointRouteBuilder endpoints) => endpoints.ServiceProvider.CheckLicense();
|
||||
|
||||
internal static void CheckLicense(this IServiceProvider serviceProvider)
|
||||
{
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
var options = serviceProvider.GetRequiredService<IOptions<BffOptions>>().Value;
|
||||
|
||||
var license = serviceProvider.GetRequiredService<LicenseValidator>();
|
||||
if (!license.IsValid())
|
||||
{
|
||||
// Todo, enforce license validation
|
||||
}
|
||||
license.CheckLicense();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,4 +179,9 @@ public sealed class BffOptions
|
|||
/// </summary>
|
||||
public Collection<string> AllowedSilentLoginReferers { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Options for diagnostics logging
|
||||
/// </summary>
|
||||
public DiagnosticsOptions Diagnostics { get; set; } = new();
|
||||
|
||||
}
|
||||
|
|
|
|||
23
bff/src/Bff/Configuration/DiagnosticsOptions.cs
Normal file
23
bff/src/Bff/Configuration/DiagnosticsOptions.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Duende.Bff.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Options that control the way that diagnostic data is logged.
|
||||
/// </summary>
|
||||
public sealed class DiagnosticsOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Frequency at which diagnostic summaries are logged.
|
||||
/// Defaults to 1 hour.
|
||||
/// </summary>
|
||||
public TimeSpan LogFrequency { get; set; } = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>
|
||||
/// Max size of diagnostic data log message chunks in kilobytes.
|
||||
/// Defaults to 8160 bytes. 8 KB is a conservative limit for the max size of a log message that is imposed by
|
||||
/// some logging tools. We take 32 bytes less than that to allow for additional formatting of the log message.
|
||||
/// </summary>
|
||||
public int ChunkSize { get; set; } = 1024 * 8 - 32;
|
||||
}
|
||||
6
bff/src/Bff/Diagnostics/DiagnosticContext.cs
Normal file
6
bff/src/Bff/Diagnostics/DiagnosticContext.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Duende.Bff.Diagnostics;
|
||||
|
||||
internal record DiagnosticContext(DateTime ServerStartTime, DateTime CurrentServerTime);
|
||||
30
bff/src/Bff/Diagnostics/DiagnosticDataService.cs
Normal file
30
bff/src/Bff/Diagnostics/DiagnosticDataService.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Duende.Bff.Diagnostics;
|
||||
|
||||
internal class DiagnosticDataService(DateTime serverStartTime, IEnumerable<IDiagnosticEntry> entries)
|
||||
{
|
||||
public async Task<ReadOnlyMemory<byte>> GetJsonBytesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bufferWriter = new ArrayBufferWriter<byte>();
|
||||
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
|
||||
|
||||
writer.WriteStartObject();
|
||||
|
||||
var diagnosticContext = new DiagnosticContext(serverStartTime, DateTime.UtcNow);
|
||||
foreach (var diagnosticEntry in entries)
|
||||
{
|
||||
diagnosticEntry.Write(diagnosticContext, writer);
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
|
||||
return bufferWriter.WrittenMemory;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Duende.Bff.Diagnostics.DiagnosticEntries;
|
||||
|
||||
internal class AssemblyInfoDiagnosticEntry : IDiagnosticEntry
|
||||
{
|
||||
private readonly IReadOnlyList<string> _exactMatches =
|
||||
[
|
||||
"Microsoft.AspNetCore"
|
||||
];
|
||||
|
||||
private readonly IReadOnlyList<string> _startsWithMatches =
|
||||
[
|
||||
"Duende.",
|
||||
"Microsoft.AspNetCore.Components.",
|
||||
"Microsoft.AspNetCore.Authentication.",
|
||||
"Microsoft.IdentityModel.",
|
||||
"System.IdentityModel.",
|
||||
"System.IdentityModel",
|
||||
"Microsoft.EntityFrameworkCore",
|
||||
];
|
||||
|
||||
public void Write(DiagnosticContext _, Utf8JsonWriter writer)
|
||||
{
|
||||
var assemblies = GetAssemblyInfo();
|
||||
writer.WriteStartObject("AssemblyInfo");
|
||||
writer.WriteString("DotNetVersion", RuntimeInformation.FrameworkDescription);
|
||||
writer.WriteString("BFF",
|
||||
typeof(DiagnosticHostedService).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
|
||||
.InformationalVersion);
|
||||
|
||||
writer.WriteStartArray("Assemblies");
|
||||
foreach (var assembly in assemblies.Where(assembly => assembly.GetName().Name != null &&
|
||||
(_exactMatches.Contains(assembly.GetName().Name) ||
|
||||
_startsWithMatches.Any(prefix =>
|
||||
assembly.GetName().Name!.StartsWith(prefix,
|
||||
StringComparison.Ordinal)))))
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("Name", assembly.GetName().Name);
|
||||
writer.WriteString("Version", assembly.GetName().Version?.ToString() ?? "Unknown");
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static List<Assembly> GetAssemblyInfo()
|
||||
{
|
||||
var assemblies = AssemblyLoadContext.Default.Assemblies
|
||||
.OrderBy(a => a.FullName)
|
||||
.ToList();
|
||||
|
||||
return assemblies;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Duende.Bff.Diagnostics.DiagnosticEntries;
|
||||
|
||||
internal class BasicServerInfoDiagnosticEntry(TimeProvider timeProvider)
|
||||
: IDiagnosticEntry
|
||||
{
|
||||
public void Write(DiagnosticContext context, Utf8JsonWriter writer)
|
||||
{
|
||||
writer.WriteStartObject("BasicServerInfo");
|
||||
|
||||
writer.WriteString("HostName", Dns.GetHostName());
|
||||
writer.WriteString("ServerStartTime", context.ServerStartTime.ToString("o"));
|
||||
writer.WriteString("CurrentServerTime", timeProvider.GetUtcNow().UtcDateTime.ToString("o"));
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Text.Json;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
|
||||
namespace Duende.Bff.Diagnostics.DiagnosticEntries;
|
||||
|
||||
internal class FrontendCountDiagnosticEntry(IFrontendCollection frontendCollection)
|
||||
: IDiagnosticEntry
|
||||
{
|
||||
public void Write(DiagnosticContext context, Utf8JsonWriter writer) =>
|
||||
writer.WriteNumber("FrontendCount", frontendCollection.Count);
|
||||
}
|
||||
49
bff/src/Bff/Diagnostics/DiagnosticHostedService.cs
Normal file
49
bff/src/Bff/Diagnostics/DiagnosticHostedService.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Duende.Bff.Diagnostics;
|
||||
|
||||
internal class DiagnosticHostedService(
|
||||
IOptions<BffOptions> options,
|
||||
DiagnosticSummary diagnosticsSummary,
|
||||
ILogger<DiagnosticHostedService> logger,
|
||||
TimeProvider timeProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(options.Value.Diagnostics.LogFrequency, timeProvider);
|
||||
try
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
try
|
||||
{
|
||||
await diagnosticsSummary.PrintSummaryAsync(stoppingToken);
|
||||
}
|
||||
#pragma warning disable CA1031
|
||||
// Catching general exceptions here to prevent the host from crashing.
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031
|
||||
{
|
||||
logger.FailedToLogDiagnosticsSummary(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// When stopping this hosted service, "await timer.WaitForNextTickAsync(stoppingToken)" can throw an OperationCanceledException.
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await diagnosticsSummary.PrintSummaryAsync(cancellationToken);
|
||||
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
41
bff/src/Bff/Diagnostics/DiagnosticSummary.cs
Normal file
41
bff/src/Bff/Diagnostics/DiagnosticSummary.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Text;
|
||||
using Duende.Bff.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Duende.Bff.Diagnostics;
|
||||
|
||||
internal class DiagnosticSummary(
|
||||
DiagnosticDataService diagnosticDataService,
|
||||
IOptions<BffOptions> options,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
private readonly ILogger _logger = loggerFactory.CreateLogger("Duende.BFF.Diagnostics.Summary");
|
||||
|
||||
public async Task PrintSummaryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bffOptions = options.Value;
|
||||
var jsonMemory = await diagnosticDataService.GetJsonBytesAsync(cancellationToken);
|
||||
var span = jsonMemory.Span;
|
||||
|
||||
var chunkSize = bffOptions.Diagnostics.ChunkSize;
|
||||
if (span.Length > chunkSize)
|
||||
{
|
||||
var totalChunks = (span.Length + bffOptions.Diagnostics.ChunkSize - 1) / chunkSize;
|
||||
for (var i = 0; i < totalChunks; i++)
|
||||
{
|
||||
var offset = i * chunkSize;
|
||||
var length = Math.Min(chunkSize, span.Length - offset);
|
||||
var chunk = span.Slice(offset, length);
|
||||
_logger.DiagnosticSummaryLogged(i + 1, totalChunks, Encoding.UTF8.GetString(chunk));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.DiagnosticSummaryLogged(1, 1, Encoding.UTF8.GetString(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
16
bff/src/Bff/Diagnostics/DiagnosticsLog.cs
Normal file
16
bff/src/Bff/Diagnostics/DiagnosticsLog.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
// using Microsoft.Extensions.Logging;
|
||||
//
|
||||
// namespace Duende.Bff.Diagnostics;
|
||||
//
|
||||
// internal static partial class DiagnosticsLog
|
||||
// {
|
||||
// [LoggerMessage(1, LogLevel.Information, "Diagnostic summary ({current}/{total}): {diagnosticData}")]
|
||||
// internal static partial void DiagnosticSummaryLogged(this ILogger logger, int current, int total,
|
||||
// string diagnosticData);
|
||||
//
|
||||
// [LoggerMessage(2, LogLevel.Warning, "An error occurred while logging the diagnostic summary: {Message}")]
|
||||
// internal static partial void FailedToLogDiagnosticsSummary(this ILogger logger, string message);
|
||||
// }
|
||||
11
bff/src/Bff/Diagnostics/IDiagnosticEntry.cs
Normal file
11
bff/src/Bff/Diagnostics/IDiagnosticEntry.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Duende.Bff.Diagnostics;
|
||||
|
||||
internal interface IDiagnosticEntry
|
||||
{
|
||||
public void Write(DiagnosticContext context, Utf8JsonWriter writer);
|
||||
}
|
||||
|
|
@ -61,13 +61,6 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
.Where(frontend => oldFrontends.All(x => x.Name != frontend.Name))
|
||||
.ToArray();
|
||||
|
||||
var totalFrontends = oldFrontends.Length - removedFrontends.Length;
|
||||
|
||||
foreach (var frontend in addedFrontends)
|
||||
{
|
||||
_licenseValidator.LogFrontendAdded(frontend.Name, ++totalFrontends);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _frontends, newFrontends);
|
||||
|
||||
}
|
||||
|
|
@ -160,7 +153,7 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
public void AddOrUpdate(BffFrontend frontend)
|
||||
{
|
||||
var existingUpdated = false;
|
||||
// Lock to avoid dirty writes from multiple threads.
|
||||
// Lock to avoid dirty writes from multiple threads.
|
||||
lock (_syncRoot)
|
||||
{
|
||||
var existing = _frontends.FirstOrDefault(x => x.Name == frontend.Name);
|
||||
|
|
@ -188,7 +181,6 @@ internal class FrontendCollection : IDisposable, IFrontendCollection
|
|||
}
|
||||
else
|
||||
{
|
||||
_licenseValidator.LogFrontendAdded(frontend.Name, _frontends.Length);
|
||||
OnFrontendAdded(frontend);
|
||||
}
|
||||
|
||||
|
|
|
|||
6
bff/src/Bff/Licensing/GetLicenseKey.cs
Normal file
6
bff/src/Bff/Licensing/GetLicenseKey.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
internal delegate string? GetLicenseKey();
|
||||
72
bff/src/Bff/Licensing/License.cs
Normal file
72
bff/src/Bff/Licensing/License.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
/// <summary>
|
||||
/// Models a Duende commercial license.
|
||||
/// </summary>
|
||||
internal class License
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the license from the claims in a key.
|
||||
/// </summary>
|
||||
internal License(ClaimsPrincipal claims)
|
||||
{
|
||||
if (int.TryParse(claims.FindFirst(LicenseClaimTypes.Id)?.Value, out var id))
|
||||
{
|
||||
SerialNumber = id;
|
||||
}
|
||||
|
||||
CompanyName = claims.FindFirst(LicenseClaimTypes.CompanyName)?.Value;
|
||||
ContactInfo = claims.FindFirst(LicenseClaimTypes.ContactInfo)?.Value;
|
||||
|
||||
if (long.TryParse(claims.FindFirst(LicenseClaimTypes.Expiration)?.Value, out var exp))
|
||||
{
|
||||
Expiration = DateTimeOffset.FromUnixTimeSeconds(exp);
|
||||
}
|
||||
|
||||
// IsConfigured needs to be set prior to checking for clients and issuers claims or the Redistribution check will not return an appropriate value
|
||||
IsConfigured = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The serial number
|
||||
/// </summary>
|
||||
public int? SerialNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The company name
|
||||
/// </summary>
|
||||
public string? CompanyName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The company contact info
|
||||
/// </summary>
|
||||
public string? ContactInfo { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration
|
||||
/// </summary>
|
||||
public DateTimeOffset? Expiration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Extras
|
||||
/// </summary>
|
||||
public string? Extras { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the license was configured in options or from a file, and false otherwise.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true,
|
||||
nameof(SerialNumber),
|
||||
nameof(CompanyName),
|
||||
nameof(ContactInfo),
|
||||
nameof(Expiration),
|
||||
nameof(Extras))
|
||||
]
|
||||
public bool IsConfigured { get; init; }
|
||||
}
|
||||
101
bff/src/Bff/Licensing/LicenseAccessor.cs
Normal file
101
bff/src/Bff/Licensing/LicenseAccessor.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the license from configuration or a file, and validates its contents.
|
||||
/// </summary>
|
||||
internal class LicenseAccessor(GetLicenseKey getLicenseKey, ILogger<LicenseAccessor> logger)
|
||||
{
|
||||
private static readonly string[] LicenseFileNames =
|
||||
[
|
||||
"Duende_License.key",
|
||||
"Duende_IdentityServer_License.key",
|
||||
];
|
||||
|
||||
private License? _license;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public License Current => _license ??= Initialize();
|
||||
|
||||
private License Initialize()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_license != null)
|
||||
{
|
||||
return _license;
|
||||
}
|
||||
|
||||
var key = getLicenseKey() ?? LoadLicenseKeyFromFile();
|
||||
if (key == null)
|
||||
{
|
||||
return new License(new ClaimsPrincipal(new ClaimsIdentity()))
|
||||
{
|
||||
IsConfigured = false
|
||||
};
|
||||
}
|
||||
|
||||
var licenseClaims = ValidateKey(key);
|
||||
return new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims)));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? LoadLicenseKeyFromFile()
|
||||
{
|
||||
foreach (var name in LicenseFileNames)
|
||||
{
|
||||
var path = Path.Combine(Directory.GetCurrentDirectory(), name);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return File.ReadAllText(path).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Claim[] ValidateKey(string licenseKey)
|
||||
{
|
||||
var handler = new JsonWebTokenHandler();
|
||||
|
||||
var rsa = new RSAParameters
|
||||
{
|
||||
Exponent = Convert.FromBase64String("AQAB"),
|
||||
Modulus = Convert.FromBase64String(
|
||||
"tAHAfvtmGBng322TqUXF/Aar7726jFELj73lywuCvpGsh3JTpImuoSYsJxy5GZCRF7ppIIbsJBmWwSiesYfxWxBsfnpOmAHU3OTMDt383mf0USdqq/F0yFxBL9IQuDdvhlPfFcTrWEL0U2JsAzUjt9AfsPHNQbiEkOXlIwtNkqMP2naynW8y4WbaGG1n2NohyN6nfNb42KoNSR83nlbBJSwcc3heE3muTt3ZvbpguanyfFXeoP6yyqatnymWp/C0aQBEI5kDahOU641aDiSagG7zX1WaF9+hwfWCbkMDKYxeSWUkQOUOdfUQ89CQS5wrLpcU0D0xf7/SrRdY2TRHvQ=="),
|
||||
};
|
||||
|
||||
var key = new RsaSecurityKey(rsa)
|
||||
{
|
||||
KeyId = "IdentityServerLicensekey/7ceadbb78130469e8806891025414f16"
|
||||
};
|
||||
|
||||
var parms = new TokenValidationParameters
|
||||
{
|
||||
ValidIssuer = "https://duendesoftware.com",
|
||||
ValidAudience = "IdentityServer",
|
||||
IssuerSigningKey = key,
|
||||
|
||||
#pragma warning disable CA5404 // CA5404: Do not use ValidateLifetime in TokenValidationParameters
|
||||
// We're validating the lifetime somewhere else.
|
||||
ValidateLifetime = false
|
||||
#pragma warning restore CA5404
|
||||
};
|
||||
|
||||
var validateResult = handler.ValidateTokenAsync(licenseKey, parms).Result;
|
||||
if (!validateResult.IsValid)
|
||||
{
|
||||
logger.ErrorValidatingLicenseKey(LogLevel.Error, validateResult.Exception);
|
||||
}
|
||||
|
||||
return validateResult.ClaimsIdentity?.Claims.ToArray() ?? [];
|
||||
}
|
||||
}
|
||||
12
bff/src/Bff/Licensing/LicenseClaimTypes.cs
Normal file
12
bff/src/Bff/Licensing/LicenseClaimTypes.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
internal static class LicenseClaimTypes
|
||||
{
|
||||
public const string Id = "id";
|
||||
public const string CompanyName = "company_name";
|
||||
public const string ContactInfo = "contact_info";
|
||||
public const string Expiration = "exp";
|
||||
}
|
||||
|
|
@ -1,38 +1,33 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Duende.Private.Licensing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Duende.Bff.Licensing;
|
||||
|
||||
internal class LicenseValidator(ILogger<LicenseValidator> logger, BffLicense license, TimeProvider timeProvider)
|
||||
internal class LicenseValidator(ILogger<LicenseValidator> logger, License license, TimeProvider timeProvider)
|
||||
{
|
||||
internal LicenseValidator(ILogger<LicenseValidator> logger, ClaimsPrincipal claims, TimeProvider timeProvider)
|
||||
: this(logger, new BffLicense(claims), timeProvider)
|
||||
: this(logger, new License(claims), timeProvider)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private bool? _licenseCheckResult;
|
||||
|
||||
public bool IsValid()
|
||||
public bool CheckLicense()
|
||||
{
|
||||
if (_licenseCheckResult != null)
|
||||
{
|
||||
return _licenseCheckResult.Value;
|
||||
}
|
||||
_licenseCheckResult = CheckLicense();
|
||||
return _licenseCheckResult.Value;
|
||||
|
||||
_licenseCheckResult = CheckLicenseValidity();
|
||||
return _licenseCheckResult.Value;
|
||||
}
|
||||
|
||||
private bool CheckLicense()
|
||||
private bool CheckLicenseValidity()
|
||||
{
|
||||
|
||||
if (!license.IsConfigured)
|
||||
{
|
||||
logger.NoValidLicense(LogLevel.Error);
|
||||
|
|
@ -41,55 +36,11 @@ internal class LicenseValidator(ILogger<LicenseValidator> logger, BffLicense lic
|
|||
|
||||
if (license.Expiration <= timeProvider.GetUtcNow())
|
||||
{
|
||||
logger.LicenseHasExpired(LogLevel.Error, license.Expiration, license.ContactInfo, license.CompanyName);
|
||||
logger.LicenseHasExpired(LogLevel.Error, license.Expiration, license.ContactInfo ?? "",
|
||||
license.CompanyName ?? "");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!license.BffFeature)
|
||||
{
|
||||
logger.NotLicensedForBff(LogLevel.Error, license.ContactInfo, license.CompanyName);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.LicenseDetails(
|
||||
LogLevel.Debug,
|
||||
license.Edition.ToString(),
|
||||
license.Expiration,
|
||||
license.ContactInfo,
|
||||
license.CompanyName,
|
||||
license.FrontendLimit switch
|
||||
{
|
||||
null => "not licensed for multi-frontend feature",
|
||||
0 => "not licensed for multi-frontend feature",
|
||||
-1 => "unlimited",
|
||||
> 0 => license.FrontendLimit.Value.ToString(CultureInfo.InvariantCulture),
|
||||
// Should't happen, but just in case
|
||||
_ => "not licensed for multi-frontend feature"
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void LogFrontendAdded(BffFrontendName frontendName, int frontendCount)
|
||||
{
|
||||
if (license?.FrontendLimit == null)
|
||||
{
|
||||
logger.NotLicensedForMultiFrontend(LogLevel.Error, frontendName);
|
||||
return;
|
||||
}
|
||||
if (license.FrontendLimit == -1)
|
||||
{
|
||||
// unlimited frontends
|
||||
logger.UnlimitedFrontends(LogLevel.Debug, frontendName, frontendCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if (license.FrontendLimit < frontendCount)
|
||||
{
|
||||
logger.FrontendLimitExceeded(LogLevel.Error, frontendName, frontendCount, license.FrontendLimit.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.FrontendAdded(LogLevel.Debug, frontendName, frontendCount, license.FrontendLimit.Value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
// using Microsoft.Extensions.Logging;
|
||||
|
||||
//
|
||||
// namespace Duende.Bff.Licensing;
|
||||
|
||||
//
|
||||
// internal static partial class LicensingLogMessages
|
||||
// {
|
||||
// [LoggerMessage(
|
||||
|
|
@ -19,9 +19,9 @@
|
|||
// public static partial void LicenseDetails(this ILogger logger, LogLevel level, string? edition, DateTimeOffset? expirationDate, string licenseContact, string licenseCompany, string? numberOfFrontends);
|
||||
//
|
||||
// [LoggerMessage(
|
||||
// Message = """
|
||||
// Your license for Duende BFF Security Framework has expired on {ExpirationDate}.
|
||||
// Please contact {licenseContact} from {licenseCompany} to obtain a valid license for the Duende software,
|
||||
// Message = $$"""
|
||||
// Your license for the Duende Software has expired on {ExpirationDate}.
|
||||
// Please contact {licenseContact} from {licenseCompany} to obtain a valid license for Duende software,
|
||||
// or start a conversation with us: https://duende.link/l/bff/contact
|
||||
//
|
||||
// See https://duende.link/l/bff/expired for more information.
|
||||
|
|
@ -31,12 +31,10 @@
|
|||
//
|
||||
// [LoggerMessage(
|
||||
// message: """
|
||||
// You do not have a valid license key for the Duende BFF Security Framework.
|
||||
// When unlicensed, BFF will run in trial mode. It will limit the number of active sessions to 5.
|
||||
// You do not have a valid license key for the Duende software.
|
||||
// This is allowed for development and testing scenarios.
|
||||
// If you are running in production you are required to have a licensed version.
|
||||
// Please start a conversation with us: https://duende.link/l/bff/contact
|
||||
//
|
||||
// See https://duende.link/l/bff/trial for more information.
|
||||
// Please start a conversation with us: https://duende.link/l/contact"
|
||||
// """)]
|
||||
// public static partial void NoValidLicense(this ILogger logger, LogLevel logLevel);
|
||||
//
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,7 @@
|
|||
// using Duende.Bff.SessionManagement.SessionStore;
|
||||
// using Microsoft.AspNetCore.Http;
|
||||
// using Microsoft.Extensions.Logging;
|
||||
|
||||
//
|
||||
// namespace Duende.Bff.Otel;
|
||||
//
|
||||
// internal static partial class LogMessages
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ public static class ServiceCollectionExtensions
|
|||
|
||||
return builder
|
||||
.AddBaseBffServices()
|
||||
.AddDynamicFrontends();
|
||||
.AddDynamicFrontends()
|
||||
.AddDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Duende.Bff.AccessTokenManagement;
|
|||
using Duende.Bff.Blazor;
|
||||
using Duende.Bff.Blazor.Client;
|
||||
using Duende.Bff.Blazor.Client.Internals;
|
||||
using Duende.Bff.Diagnostics;
|
||||
using Duende.Bff.DynamicFrontends.Internal;
|
||||
using Duende.Bff.Endpoints.Internal;
|
||||
using Duende.Bff.EntityFramework;
|
||||
|
|
@ -25,13 +26,14 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
public static readonly Assembly BffBlazorClientAssembly = typeof(BffBlazorClientOptions).Assembly;
|
||||
public static readonly Assembly BffEntityFrameworkAssembly = typeof(UserSessionEntity).Assembly;
|
||||
public static readonly Assembly BffYarpAssembly = typeof(BffYarpTransformBuilder).Assembly;
|
||||
|
||||
public static readonly Type[] AllTypes =
|
||||
BffAssembly.GetTypes()
|
||||
.Union(BffBlazorAssembly.GetTypes())
|
||||
.Union(BffBlazorClientAssembly.GetTypes())
|
||||
.Union(BffEntityFrameworkAssembly.GetTypes())
|
||||
.Union(BffYarpAssembly.GetTypes())
|
||||
.ToArray();
|
||||
.ToArray();
|
||||
|
||||
[Fact]
|
||||
public void All_strongly_typed_strings_Have_private_value()
|
||||
|
|
@ -66,7 +68,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
{
|
||||
var buildMethod = type.GetMethods(BindingFlags.Static)
|
||||
.FirstOrDefault(m => m.Name == "Create");
|
||||
buildMethod.ShouldBeNull("The IStonglyTypedString defines a Create method, but it should be implemented explicitly on the interface, not on the type. \r\n IE: " +
|
||||
buildMethod.ShouldBeNull(
|
||||
"The IStonglyTypedString defines a Create method, but it should be implemented explicitly on the interface, not on the type. \r\n IE: " +
|
||||
" static AccessTokenString IStonglyTypedString<AccessTokenString>.Create(string result) => new(result);");
|
||||
}
|
||||
}
|
||||
|
|
@ -83,9 +86,11 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
|
||||
// Try to invoke the constructor with a value and expect an exception
|
||||
var ex = Should.Throw<TargetInvocationException>(() => ctor.Invoke([]));
|
||||
ex.InnerException.ShouldBeOfType<InvalidOperationException>().Message.ShouldContain("Can't create null value");
|
||||
ex.InnerException.ShouldBeOfType<InvalidOperationException>().Message
|
||||
.ShouldContain("Can't create null value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_strongly_typed_strings_should_have_only_expected_constructors()
|
||||
{
|
||||
|
|
@ -96,7 +101,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
var ctors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
// There must be exactly two constructors
|
||||
ctors.Length.ShouldBe(2, $"{type.Name} should have exactly two constructors: one public parameterless and one private with a single string parameter.");
|
||||
ctors.Length.ShouldBe(2,
|
||||
$"{type.Name} should have exactly two constructors: one public parameterless and one private with a single string parameter.");
|
||||
|
||||
// Find the public parameterless constructor
|
||||
var publicParameterlessCtor = ctors.FirstOrDefault(c =>
|
||||
|
|
@ -111,7 +117,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
c.GetParameters().Length == 1 &&
|
||||
c.GetParameters()[0].ParameterType == typeof(string));
|
||||
|
||||
privateStringCtor.ShouldNotBeNull($"{type.Name} should have a private constructor with a single string parameter.");
|
||||
privateStringCtor.ShouldNotBeNull(
|
||||
$"{type.Name} should have a private constructor with a single string parameter.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,13 +142,15 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
[Fact()]
|
||||
public void All_types_not_in_Internal_namespace_should_be_sealed_or_static()
|
||||
{
|
||||
Type[] exclusions = [
|
||||
Type[] exclusions =
|
||||
[
|
||||
typeof(SessionDbContext),
|
||||
typeof(SessionDbContext<>),
|
||||
typeof(UserSessionEntity),
|
||||
typeof(UserSession),
|
||||
typeof(UserSessionUpdate),
|
||||
typeof(AccessTokenRetrievalError)];
|
||||
typeof(AccessTokenRetrievalError)
|
||||
];
|
||||
|
||||
// Find all types NOT in a '.Internal' namespace
|
||||
var nonInternalTypes = AllTypes
|
||||
|
|
@ -169,7 +178,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
public void All_async_methods_should_end_with_Async_and_have_cancellation_token_as_last_parameter()
|
||||
{
|
||||
var failures = new List<string>();
|
||||
Type[] exclusions = [
|
||||
Type[] exclusions =
|
||||
[
|
||||
typeof(BffAuthenticationSchemeProvider),
|
||||
typeof(BffOpenIdConnectEvents),
|
||||
typeof(BffAuthenticationService),
|
||||
|
|
@ -182,8 +192,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
typeof(BffCacheClearingHostedService),
|
||||
typeof(SessionDbContext),
|
||||
typeof(SessionDbContext<>),
|
||||
typeof(DiagnosticHostedService),
|
||||
typeof(ServerSideTokenStore), // This one needs to be removed after move to ATM 4.0
|
||||
|
||||
];
|
||||
foreach (var type in AllTypes
|
||||
.Where(t => !exclusions.Contains(t))
|
||||
|
|
@ -206,7 +216,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 0 || parameters.Last().ParameterType != typeof(CT))
|
||||
{
|
||||
failures.Add($"{type.FullName}.{method.Name}: Async method should have a CT as the last parameter.");
|
||||
failures.Add(
|
||||
$"{type.FullName}.{method.Name}: Async method should have a CT as the last parameter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -218,18 +229,18 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
|
||||
failures.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
public static bool IsInternal(Type type)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return type.IsNestedPrivate || type.IsNotPublic;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#nullable disable
|
||||
[Fact]
|
||||
public void AccessTokenManagement_is_not_exposed()
|
||||
|
|
@ -252,7 +263,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
}
|
||||
|
||||
// Check public members for forbidden types
|
||||
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
|
||||
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static |
|
||||
BindingFlags.DeclaredOnly);
|
||||
foreach (var member in members)
|
||||
{
|
||||
switch (member)
|
||||
|
|
@ -263,30 +275,36 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsForbiddenType(method.ReturnType))
|
||||
{
|
||||
errors.Add($"{type.FullName}.{method.Name} returns forbidden type {method.ReturnType.FullName}");
|
||||
errors.Add(
|
||||
$"{type.FullName}.{method.Name} returns forbidden type {method.ReturnType.FullName}");
|
||||
}
|
||||
|
||||
foreach (var param in method.GetParameters())
|
||||
{
|
||||
if (IsForbiddenType(param.ParameterType))
|
||||
{
|
||||
errors.Add($"{type.FullName}.{method.Name} parameter '{param.Name}' is forbidden type {param.ParameterType.FullName}");
|
||||
errors.Add(
|
||||
$"{type.FullName}.{method.Name} parameter '{param.Name}' is forbidden type {param.ParameterType.FullName}");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case PropertyInfo prop:
|
||||
if (IsForbiddenType(prop.PropertyType))
|
||||
{
|
||||
errors.Add($"{type.FullName}.{prop.Name} property is forbidden type {prop.PropertyType.FullName}");
|
||||
errors.Add(
|
||||
$"{type.FullName}.{prop.Name} property is forbidden type {prop.PropertyType.FullName}");
|
||||
}
|
||||
|
||||
break;
|
||||
case FieldInfo field:
|
||||
if (IsForbiddenType(field.FieldType))
|
||||
{
|
||||
errors.Add($"{type.FullName}.{field.Name} field is forbidden type {field.FieldType.FullName}");
|
||||
errors.Add(
|
||||
$"{type.FullName}.{field.Name} field is forbidden type {field.FieldType.FullName}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -294,7 +312,8 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
|
||||
if (IsForbiddenType(evt.EventHandlerType!))
|
||||
{
|
||||
errors.Add($"{type.FullName}.{evt.Name} event is forbidden type {evt.EventHandlerType.FullName}");
|
||||
errors.Add(
|
||||
$"{type.FullName}.{evt.Name} event is forbidden type {evt.EventHandlerType.FullName}");
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
@ -331,10 +350,12 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (t.IsArray)
|
||||
{
|
||||
return IsForbiddenType(t.GetElementType()!);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -346,15 +367,16 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
// Find all types implementing IStringValue<TSelf>
|
||||
var stringValueTypes =
|
||||
AllTypes.Where(t => t.IsValueType && !t.IsAbstract)
|
||||
.SelectMany(t =>
|
||||
t.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStronglyTypedValue<>)
|
||||
&& i.GenericTypeArguments[0] == t)
|
||||
.Select(_ => t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
.SelectMany(t =>
|
||||
t.GetInterfaces()
|
||||
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStronglyTypedValue<>)
|
||||
&& i.GenericTypeArguments[0] == t)
|
||||
.Select(_ => t))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
return stringValueTypes;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_interface_async_methods_should_have_cancellation_token_with_default()
|
||||
{
|
||||
|
|
@ -368,18 +390,22 @@ public class ConventionTests(ITestOutputHelper output)
|
|||
var parameters = method.GetParameters();
|
||||
if (parameters.Length == 0)
|
||||
{
|
||||
failures.Add($"{type.FullName}.{method.Name}: Async method should have a CancellationToken parameter with a default value.");
|
||||
failures.Add(
|
||||
$"{type.FullName}.{method.Name}: Async method should have a CancellationToken parameter with a default value.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var ctParam = parameters.Last();
|
||||
if (ctParam.ParameterType != typeof(System.Threading.CancellationToken))
|
||||
{
|
||||
failures.Add($"{type.FullName}.{method.Name}: Last parameter should be CancellationToken.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ctParam.HasDefaultValue)
|
||||
{
|
||||
failures.Add($"{type.FullName}.{method.Name}: CancellationToken parameter should have a default value.");
|
||||
failures.Add(
|
||||
$"{type.FullName}.{method.Name}: CancellationToken parameter should have a default value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Duende.Bff.Tests.TestInfra;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Duende.Bff.Tests.Diagnostics;
|
||||
|
||||
public class FrontendCountDiagnosticEntryTests(ITestOutputHelper testOutputHelper) : BffTestBase(testOutputHelper)
|
||||
{
|
||||
[Fact]
|
||||
public async Task Should_print_the_number_of_frontends_during_defined_interval()
|
||||
{
|
||||
Bff.OnConfigureBffOptions += options =>
|
||||
{
|
||||
options.Diagnostics.LogFrequency = TimeSpan.FromHours(1);
|
||||
};
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
AdvanceClock(TimeSpan.FromHours(1));
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
|
||||
bffLogMessages.ShouldContain(x => x.Contains("\"FrontendCount\":0"));
|
||||
|
||||
AddOrUpdateFrontend(new BffFrontend
|
||||
{
|
||||
Name = BffFrontendName.Parse("frontend1"),
|
||||
});
|
||||
AddOrUpdateFrontend(new BffFrontend
|
||||
{
|
||||
Name = BffFrontendName.Parse("frontend2"),
|
||||
});
|
||||
|
||||
AdvanceClock(TimeSpan.FromHours(1));
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
|
||||
bffLogMessages.ShouldContain(x => x.Contains("\"FrontendCount\":2"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Security.Claims;
|
||||
using Duende.Bff.DynamicFrontends;
|
||||
using Duende.Bff.Licensing;
|
||||
using Duende.Bff.Tests.TestInfra;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Xunit.Abstractions;
|
||||
|
|
@ -18,112 +15,38 @@ public class LicensingTests(ITestOutputHelper output) : BffTestBase(output)
|
|||
Bff.LicenseKey = null;
|
||||
|
||||
await InitializeAsync();
|
||||
Context.LogMessages.ToString().ShouldContain("You do not have a valid license key for the Duende BFF security framework");
|
||||
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
|
||||
bffLogMessages.ShouldContain(x => x.Contains("You do not have a valid license key for the Duende software."));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Given_expired_license_then_log_error()
|
||||
{
|
||||
|
||||
Bff.LicenseKey = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
|
||||
Bff.LicenseKey =
|
||||
"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
|
||||
|
||||
await InitializeAsync();
|
||||
Context.LogMessages.ToString().ShouldContain("Your license for Duende BFF security framework has expired on");
|
||||
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"));
|
||||
bffLogMessages.ShouldContain(x => x.Contains("Your license for the Duende Software has expired on "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Given_valid_license_then_details()
|
||||
{
|
||||
|
||||
SetupValidLicenseWithoutFrontends();
|
||||
|
||||
await InitializeAsync();
|
||||
|
||||
var log = Context.LogMessages.ToString();
|
||||
log.ShouldContain("Duende BFF security Framework License information:");
|
||||
log.ShouldContain("- Edition: Starter");
|
||||
log.ShouldContain("- Expiration: 11/15/2024 00:00:00 +00:00");
|
||||
log.ShouldContain("- LicenseContact: joe@duendesoftware.com");
|
||||
log.ShouldContain("- LicenseContact: joe@duendesoftware.com");
|
||||
log.ShouldContain("- Number of frontends licensed: not licensed for multi-frontend feature");
|
||||
var bffLogMessages = Context.LogMessages.ToString().Split(Environment.NewLine).Where(x => x.StartsWith("bff"))
|
||||
.ToList();
|
||||
bffLogMessages.ShouldNotContain(x => x.Contains("You do not have a valid license key for the Duende software."));
|
||||
bffLogMessages.ShouldNotContain(x => x.Contains("Your license for the Duende Software has expired on "));
|
||||
}
|
||||
|
||||
private void SetupValidLicenseWithoutFrontends()
|
||||
{
|
||||
The.Clock = new FakeTimeProvider(new DateTimeOffset(2024, 1, 1, 1, 1, 1, TimeSpan.Zero));
|
||||
Bff.LicenseKey = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_not_licence_for_multi_frontends_then_warns()
|
||||
{
|
||||
SetupValidLicenseWithoutFrontends();
|
||||
await InitializeAsync();
|
||||
|
||||
Bff.AddOrUpdateFrontend(Some.BffFrontend());
|
||||
var log = Context.LogMessages.ToString();
|
||||
log.ShouldContain($"Frontend {The.FrontendName} was added. However, your current license does not support multiple frontends");
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_licenced_for_frontends_then_info()
|
||||
{
|
||||
SetupValidLicenseWithoutFrontends();
|
||||
|
||||
Bff.OnConfigureServices += services =>
|
||||
{
|
||||
Claim[] claims = [
|
||||
new Claim("feature", "bff"),
|
||||
new Claim("bff_frontend_limit", "10"),
|
||||
];
|
||||
|
||||
services.AddSingleton<LicenseValidator>(sp =>
|
||||
{
|
||||
return new LicenseValidator(
|
||||
logger: sp.GetRequiredService<ILogger<LicenseValidator>>(),
|
||||
claims: new ClaimsPrincipal(new ClaimsIdentity(claims)),
|
||||
timeProvider: The.Clock);
|
||||
});
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
Bff.AddOrUpdateFrontend(Some.BffFrontend());
|
||||
var log = Context.LogMessages.ToString();
|
||||
log.ShouldContain($"Frontend {The.FrontendName} was added. Currently using 1 of 10 in the BFF License");
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task When_exceeding_number_of_licensed_frontends_then_warning()
|
||||
{
|
||||
SetupValidLicenseWithoutFrontends();
|
||||
|
||||
Bff.OnConfigureServices += services =>
|
||||
{
|
||||
Claim[] claims = [
|
||||
new Claim("feature", "bff"),
|
||||
new Claim("bff_frontend_limit", "1"),
|
||||
];
|
||||
|
||||
services.AddSingleton<LicenseValidator>(sp =>
|
||||
{
|
||||
return new LicenseValidator(
|
||||
logger: sp.GetRequiredService<ILogger<LicenseValidator>>(),
|
||||
claims: new ClaimsPrincipal(new ClaimsIdentity(claims)),
|
||||
timeProvider: The.Clock);
|
||||
});
|
||||
};
|
||||
await InitializeAsync();
|
||||
|
||||
Bff.AddOrUpdateFrontend(Some.BffFrontend());
|
||||
Bff.AddOrUpdateFrontend(Some.BffFrontend() with
|
||||
{
|
||||
Name = BffFrontendName.Parse("second")
|
||||
});
|
||||
var log = Context.LogMessages.ToString();
|
||||
log.ShouldContain($"Frontend {The.FrontendName} was added. Currently using 1 of 1 in the BFF License");
|
||||
log.ShouldContain($"Frontend second was added. This exceeds the maximum number of frontends allowed by your license");
|
||||
|
||||
Bff.LicenseKey =
|
||||
"eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzA0MDY3MjAwLCJleHAiOjE3MzE2Mjg4MDAsImNvbXBhbnlfbmFtZSI6Il90ZXN0IiwiY29udGFjdF9pbmZvIjoiam9lQGR1ZW5kZXNvZnR3YXJlLmNvbSIsImVkaXRpb24iOiJTdGFydGVyIiwiaWQiOiI3ODk2IiwiZmVhdHVyZSI6ImJmZiJ9.YcRGLlVuNBSqNuO1mdXk4GvvVEQFfQUNAnTkzs9W2iNKCxLXrZ5mDPuyTNsDSwEqsfXG8bUCVFxFGp1Bfkxs8hUIBiKuVXfeIB_lmpj5f-KueZ_XlWm0pYT-ROAzVbDdNgMR9YqCPAw8ANclk7HwRcXc0VnLNcKRFrZ0OOWNysFIanTmg7hRIQmDuMLNc2j8HCZSRJ06fijecS72lM4Vv9a6myJvAsASQhKnWTLzQvdzW7T99eobLy45qJu39LMTQkPkkJUS41YPmi2_kEmeMcRucgU4dQKHD5zT9KmzPVWJwsyowWIJ6U7lZ8FXZ8c9POsQeTeQEJY6FheJ2Ut-6Q";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ namespace Duende.Bff.Configuration
|
|||
public System.Net.Http.HttpMessageHandler? BackchannelHttpHandler { get; set; }
|
||||
public bool BackchannelLogoutAllUserSessions { get; set; }
|
||||
public Duende.Bff.AccessTokenManagement.DPoPProofKey? DPoPJsonWebKey { get; set; }
|
||||
public Duende.Bff.Configuration.DiagnosticsOptions Diagnostics { get; set; }
|
||||
public System.Collections.Generic.ICollection<string> DiagnosticsEnvironments { get; }
|
||||
public Microsoft.AspNetCore.Http.PathString DiagnosticsPath { get; }
|
||||
public Duende.Bff.Configuration.DisableAntiForgeryCheck DisableAntiForgeryCheck { get; set; }
|
||||
|
|
@ -344,6 +345,12 @@ namespace Duende.Bff.Configuration
|
|||
public Duende.Bff.AccessTokenManagement.Resource? Resource { get; init; }
|
||||
public Duende.Bff.AccessTokenManagement.Scheme? SignInScheme { get; init; }
|
||||
}
|
||||
public sealed class DiagnosticsOptions
|
||||
{
|
||||
public DiagnosticsOptions() { }
|
||||
public int ChunkSize { get; set; }
|
||||
public System.TimeSpan LogFrequency { get; set; }
|
||||
}
|
||||
public delegate bool DisableAntiForgeryCheck(Microsoft.AspNetCore.Http.HttpContext context);
|
||||
}
|
||||
namespace Duende.Bff.DynamicFrontends
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public class TestDataBuilder(TestData the)
|
|||
public readonly TestData The = the;
|
||||
|
||||
internal LicenseValidator LicenseValidator =>
|
||||
new LicenseValidator(new NullLogger<LicenseValidator>(), new ClaimsPrincipal(), The.Clock);
|
||||
new(new NullLogger<LicenseValidator>(), new ClaimsPrincipal(), The.Clock);
|
||||
|
||||
public BffFrontend BffFrontend(BffFrontendName? name = null) =>
|
||||
new()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class DiagnosticDataService
|
|||
_entries = entries;
|
||||
}
|
||||
|
||||
public async Task<ReadOnlyMemory<byte>> GetJsonBytesAsync()
|
||||
public async Task<ReadOnlyMemory<byte>> GetJsonBytesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bufferWriter = new ArrayBufferWriter<byte>();
|
||||
await using var writer = new Utf8JsonWriter(bufferWriter, new JsonWriterOptions { Indented = false });
|
||||
|
|
@ -37,14 +37,14 @@ public class DiagnosticDataService
|
|||
|
||||
writer.WriteEndObject();
|
||||
|
||||
await writer.FlushAsync();
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
|
||||
return bufferWriter.WrittenMemory;
|
||||
}
|
||||
|
||||
public async Task<string> GetJsonStringAsync()
|
||||
public async Task<string> GetJsonStringAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bytes = await GetJsonBytesAsync();
|
||||
var bytes = await GetJsonBytesAsync(cancellationToken);
|
||||
return Encoding.UTF8.GetString(bytes.Span);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue