Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into chore/update-userhelper
Browse files Browse the repository at this point in the history
  • Loading branch information
danielskovli committed Nov 29, 2024
2 parents 7bd98de + fea0541 commit dab9e24
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 34 deletions.
16 changes: 13 additions & 3 deletions src/Altinn.App.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,15 @@ public async Task StartAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

/// <summary>
/// PDF generation works by using a headless browser to render the frontend of an app instance.
/// To make debugging PDF generation failures easier, we want requests originating from the PDF generator to be
/// contained in the root trace (process/next) as children. The frontend will set this header when making requests to the app backend in PDF mode.
/// </summary>
/// <param name="headers">Request headers attached to the span</param>
/// <returns></returns>
private static bool IsPdfGeneratorRequest(IHeaderDictionary headers) => headers.ContainsKey("X-Altinn-IsPdf");

internal sealed class OtelPropagator : TextMapPropagator
{
private readonly TextMapPropagator _inner;
Expand All @@ -359,8 +368,9 @@ public override PropagationContext Extract<T>(
Func<T, string, IEnumerable<string>?> getter
)
{
if (carrier is HttpRequest)
if (carrier is HttpRequest req && !IsPdfGeneratorRequest(req.Headers))
return default;

return _inner.Extract(context, carrier, getter);
}

Expand All @@ -381,7 +391,7 @@ internal sealed class AspNetCorePropagator : DistributedContextPropagator
PropagatorGetterCallback? getter
)
{
if (carrier is IHeaderDictionary)
if (carrier is IHeaderDictionary headers && !IsPdfGeneratorRequest(headers))
return null;

return _inner.ExtractBaggage(carrier, getter);
Expand All @@ -394,7 +404,7 @@ public override void ExtractTraceIdAndState(
out string? traceState
)
{
if (carrier is IHeaderDictionary)
if (carrier is IHeaderDictionary headers && !IsPdfGeneratorRequest(headers))
{
traceId = null;
traceState = null;
Expand Down
48 changes: 20 additions & 28 deletions src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ private static void AddApplicationIdentifier(IServiceCollection services)
{
services.AddSingleton(sp =>
{
var appIdentifier = GetApplicationId();
string appIdentifier = GetApplicationId();
return new AppIdentifier(appIdentifier);
});
}
Expand All @@ -132,14 +132,10 @@ private static string GetApplicationId()

var id = appMetadataJObject?.SelectToken("id")?.Value<string>();

if (id == null)
{
throw new KeyNotFoundException(
"Could not find id in applicationmetadata.json. Please ensure applicationmeta.json is well formed and contains a key for id."
return id
?? throw new KeyNotFoundException(
"Could not find id in applicationmetadata.json. Please ensure the file is well formed and contains a key for `id`"
);
}

return id;
}

/// <summary>
Expand All @@ -154,9 +150,7 @@ public static void AddAppServices(
IWebHostEnvironment env
)
{
// Services for Altinn App
services.TryAddTransient<IPDP, PDPAppSI>();
AddValidationServices(services, configuration);
services.TryAddTransient<IPrefill, PrefillSI>();
services.TryAddTransient<ISigningCredentialsResolver, SigningCredentialsResolver>();
services.TryAddSingleton<IAppResources, AppResourcesSI>();
Expand All @@ -175,13 +169,14 @@ IWebHostEnvironment env
services.TryAddTransient<ILayoutEvaluatorStateInitializer, LayoutEvaluatorStateInitializer>();
services.TryAddTransient<LayoutEvaluatorStateInitializer>();
services.AddTransient<IDataService, DataService>();
services.AddSingleton<ModelSerializationService>();
services.Configure<Common.PEP.Configuration.PepSettings>(configuration.GetSection("PEPSettings"));
services.Configure<Common.PEP.Configuration.PlatformSettings>(configuration.GetSection("PlatformSettings"));
services.Configure<AccessTokenSettings>(configuration.GetSection("AccessTokenSettings"));
services.Configure<FrontEndSettings>(configuration.GetSection(nameof(FrontEndSettings)));
services.Configure<PdfGeneratorSettings>(configuration.GetSection(nameof(PdfGeneratorSettings)));
services.AddSingleton<ModelSerializationService>();

AddValidationServices(services, configuration);
AddAppOptions(services);
AddExternalApis(services);
AddActionServices(services);
Expand Down Expand Up @@ -209,32 +204,29 @@ private static void AddValidationServices(IServiceCollection services, IConfigur
{
services.AddTransient<IValidatorFactory, ValidatorFactory>();
services.AddTransient<IValidationService, ValidationService>();
if (configuration.GetSection("AppSettings").Get<AppSettings>()?.RequiredValidation == true)
services.AddTransient<IFormDataValidator, DataAnnotationValidator>();
services.AddTransient<IDataElementValidator, DefaultDataElementValidator>();
services.AddTransient<ITaskValidator, DefaultTaskValidator>();

var appSettings = configuration.GetSection("AppSettings").Get<AppSettings>();
if (appSettings?.RequiredValidation is true)
{
services.AddTransient<IValidator, RequiredLayoutValidator>();
}

if (configuration.GetSection("AppSettings").Get<AppSettings>()?.ExpressionValidation == true)
if (appSettings?.ExpressionValidation is true)
{
services.AddTransient<IValidator, ExpressionValidator>();
}
services.AddTransient<IFormDataValidator, DataAnnotationValidator>();
services.AddTransient<IDataElementValidator, DefaultDataElementValidator>();
services.AddTransient<ITaskValidator, DefaultTaskValidator>();
}

/// <summary>
/// Checks if a service is already added to the collection.
/// </summary>
/// <returns>true if the services allready exists in the collection, otherwise false</returns>
/// <returns>true if the services already exists in the collection, otherwise false</returns>
public static bool IsAdded(this IServiceCollection services, Type serviceType)
{
if (services.Any(x => x.ServiceType == serviceType))
{
return true;
}

return false;
return services.Any(x => x.ServiceType == serviceType);
}

private static void AddEventServices(IServiceCollection services)
Expand All @@ -243,7 +235,8 @@ private static void AddEventServices(IServiceCollection services)
services.AddTransient<IEventHandlerResolver, EventHandlerResolver>();
services.TryAddSingleton<IEventSecretCodeProvider, KeyVaultEventSecretCodeProvider>();

// The event subscription client depends uppon a maskinporten messagehandler beeing
// TODO: Event subs could be handled by the new automatic Maskinporten auth, once implemented.

Check warning on line 238 in src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs

View workflow job for this annotation

GitHub Actions / Static code analysis

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
// The event subscription client depends upon a Maskinporten message handler being
// added to the client during setup. As of now this needs to be done in the apps
// if subscription is to be added. This registration is to prevent the DI container
// from failing for the apps not using event subscription. If you try to use
Expand Down Expand Up @@ -317,18 +310,17 @@ private static void AddAppOptions(IServiceCollection services)
private static void AddExternalApis(IServiceCollection services)
{
services.AddTransient<IExternalApiService, ExternalApiService>();

services.TryAddTransient<IExternalApiFactory, ExternalApiFactory>();
}

private static void AddProcessServices(IServiceCollection services)
{
services.AddTransient<IProcessExclusiveGateway, ExpressionsExclusiveGateway>();
services.TryAddTransient<IProcessEngine, ProcessEngine>();
services.TryAddTransient<IProcessNavigator, ProcessNavigator>();
services.TryAddSingleton<IProcessReader, ProcessReader>();
services.TryAddTransient<IProcessEventHandlerDelegator, ProcessEventHandlingDelegator>();
services.TryAddTransient<IProcessEventDispatcher, ProcessEventDispatcher>();
services.AddTransient<IProcessExclusiveGateway, ExpressionsExclusiveGateway>();
services.TryAddTransient<ExclusiveGatewayFactory>();

services.AddTransient<IProcessTaskInitializer, ProcessTaskInitializer>();
Expand All @@ -340,14 +332,14 @@ private static void AddProcessServices(IServiceCollection services)
services.AddTransient<IAbandonTaskEventHandler, AbandonTaskEventHandler>();
services.AddTransient<IEndEventEventHandler, EndEventEventHandler>();

//PROCESS TASKS
// Process tasks
services.AddTransient<IProcessTask, DataProcessTask>();
services.AddTransient<IProcessTask, ConfirmationProcessTask>();
services.AddTransient<IProcessTask, FeedbackProcessTask>();
services.AddTransient<IProcessTask, SigningProcessTask>();
services.AddTransient<IProcessTask, NullTypeProcessTask>();

//SERVICE TASKS
// Service tasks
services.AddTransient<IServiceTask, PdfServiceTask>();
services.AddTransient<IServiceTask, EformidlingServiceTask>();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics;

namespace Altinn.App.Core.Features;

partial class Telemetry
{
internal Activity? StartGeneratePdfClientActivity()
{
var activity = ActivitySource.StartActivity("PdfGeneratorClient.GeneratePdf");
return activity;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Features;
using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Models.Pdf;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Context.Propagation;

namespace Altinn.App.Core.Infrastructure.Clients.Pdf;

Expand All @@ -15,40 +19,50 @@ namespace Altinn.App.Core.Infrastructure.Clients.Pdf;
/// </summary>
public class PdfGeneratorClient : IPdfGeneratorClient
{
private static readonly TextMapPropagator _w3cPropagator = new TraceContextPropagator();

private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

private readonly ILogger<PdfGeneratorClient> _logger;
private readonly HttpClient _httpClient;
private readonly PdfGeneratorSettings _pdfGeneratorSettings;
private readonly PlatformSettings _platformSettings;
private readonly IUserTokenProvider _userTokenProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Telemetry? _telemetry;

/// <summary>
/// Initializes a new instance of the <see cref="PdfGeneratorClient"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="httpClient">The HttpClient to use in communication with the PDF generator service.</param>
/// <param name="pdfGeneratorSettings">
/// All generic settings needed for communication with the PDF generator service.
/// </param>
/// <param name="platformSettings">Links to platform services</param>
/// <param name="userTokenProvider">A service able to identify the JWT for currently authenticated user.</param>
/// <param name="httpContextAccessor">http context</param>
/// <param name="telemetry">Telemetry service</param>
public PdfGeneratorClient(
ILogger<PdfGeneratorClient> logger,
HttpClient httpClient,
IOptions<PdfGeneratorSettings> pdfGeneratorSettings,
IOptions<PlatformSettings> platformSettings,
IUserTokenProvider userTokenProvider,
IHttpContextAccessor httpContextAccessor
IHttpContextAccessor httpContextAccessor,
Telemetry? telemetry = null
)
{
_logger = logger;
_httpClient = httpClient;
_userTokenProvider = userTokenProvider;
_pdfGeneratorSettings = pdfGeneratorSettings.Value;
_platformSettings = platformSettings.Value;
_httpContextAccessor = httpContextAccessor;
_telemetry = telemetry;
}

/// <inheritdoc/>
Expand All @@ -60,6 +74,8 @@ public async Task<Stream> GeneratePdf(Uri uri, CancellationToken ct)
/// <inheritdoc/>
public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, CancellationToken ct)
{
using var activity = _telemetry?.StartGeneratePdfClientActivity();

bool hasWaitForSelector = !string.IsNullOrWhiteSpace(_pdfGeneratorSettings.WaitForSelector);
PdfGeneratorRequest generatorRequest = new()
{
Expand All @@ -73,6 +89,33 @@ public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, Cancellati
},
};

if (Activity.Current is { } propagateActivity)
{
// We want the frontend to attach the current trace context to requests
// when making downstream requests back to the app backend.
// This makes it easier to debug issues (such as slow backend requests during PDF generation).
// The frontend expects to see the "traceparent" and "tracestate" values as cookies (as they are easily propagated).
// It will then pass them back to the backend in the "traceparent" and "tracestate" headers as per W3C spec.
_w3cPropagator.Inject(
new PropagationContext(propagateActivity.Context, default),
generatorRequest.Cookies,
(c, k, v) =>
{
if (k != "traceparent" && k != "tracestate")
_logger.LogWarning("Unexpected key '{Key}' when propagating trace context (expected W3C)", k);

c.Add(
new PdfGeneratorCookieOptions
{
Name = $"altinn-telemetry-{k}",
Value = v,
Domain = uri.Host,
}
);
}
);
}

generatorRequest.Cookies.Add(
new PdfGeneratorCookieOptions { Value = _userTokenProvider.GetUserToken(), Domain = uri.Host }
);
Expand Down
9 changes: 9 additions & 0 deletions test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ public async Task Request_In_Dev_Should_Generate()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down Expand Up @@ -156,7 +159,10 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down Expand Up @@ -234,7 +240,10 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version()
var handler = new Mock<HttpMessageHandler>();
var httpClient = new HttpClient(handler.Object);

var logger = new Mock<ILogger<PdfGeneratorClient>>();

var pdfGeneratorClient = new PdfGeneratorClient(
logger.Object,
httpClient,
_pdfGeneratorSettingsOptions,
_platformSettingsOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@
Status: Error,
Kind: Server
},
{
ActivityName: PdfGeneratorClient.GeneratePdf,
IdFormat: W3C
},
{
ActivityName: PdfService.GenerateAndStorePdf,
Tags: [
Expand Down Expand Up @@ -392,4 +396,4 @@
}
],
Metrics: []
}
}
1 change: 1 addition & 0 deletions test/Altinn.App.Api.Tests/CustomWebApplicationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ CancellationToken cancellationToken
request.Headers,
(c, k, v) => c.TryAddWithoutValidation(k, v)
);
Assert.Contains(request.Headers, h => h.Key == "traceparent"); // traceparent is mandatory in W3C
}
return base.SendAsync(request, cancellationToken);
}
Expand Down
Loading

0 comments on commit dab9e24

Please sign in to comment.