From 0269357d4295f58d814cce94686c0d0bbc205aff Mon Sep 17 00:00:00 2001 From: Kjetil Haugland Date: Thu, 26 Sep 2024 11:33:32 +0200 Subject: [PATCH 01/44] Implement catch-all proxy for new Fusion Apps service --- ...FusionAppApiResourcesRequestTransformer.cs | 42 +++++++++++++++++ .../FusionAppResourcesRequestTransformer.cs | 45 +++++++++++++++++++ .../Configurations/AssetProxyOptions.cs | 1 + .../Configurations/FusionAppsApiOptions.cs | 9 ++++ .../Constants.cs | 1 + ...rojectExecutionPortal.ClientBackend.csproj | 2 +- .../Modules/AssetProxyConfiguration.cs | 14 +++++- .../Program.cs | 5 ++- .../Views/Bundle/Index.cshtml | 3 +- .../appsettings.json | 17 +++---- 10 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppApiResourcesRequestTransformer.cs create mode 100644 clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppResourcesRequestTransformer.cs create mode 100644 clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/FusionAppsApiOptions.cs diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppApiResourcesRequestTransformer.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppApiResourcesRequestTransformer.cs new file mode 100644 index 000000000..64722e5c4 --- /dev/null +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppApiResourcesRequestTransformer.cs @@ -0,0 +1,42 @@ +using Equinor.ProjectExecutionPortal.ClientBackend.Configurations; +using Fusion; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; +using Yarp.ReverseProxy.Forwarder; +using Yarp.ReverseProxy.Transforms; + +namespace Equinor.ProjectExecutionPortal.ClientBackend.AssetProxy +{ + public class FusionAppApiResourcesRequestTransformer : HttpTransformer + { + private readonly ITokenAcquisition _tokenAcquisition; + private readonly AssetProxyOptions _options; + + public FusionAppApiResourcesRequestTransformer(ITokenAcquisition tokenAcquisition, IOptions options) + { + _tokenAcquisition = tokenAcquisition; + _options = options.Value; + } + + public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) + { + var token = await _tokenAcquisition.GetAccessTokenForAppAsync(_options.TokenScope!); + var path = httpContext.Request.Path.Value?.Replace(Constants.FusionAppsRoute, string.Empty); + + // Copy all request headers + await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); + + // Customize the query string: + var queryContext = new QueryTransformContext(httpContext.Request); + + // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default. + proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress(_options.FusionAppsUrl!, path, queryContext.QueryString); + + proxyRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + proxyRequest.Headers.Add("X-Fusion-App-Bundle-UniqueId", $"{httpContext.User.GetAzureUniqueId()}"); + + // Suppress the original request header, use the one from the destination Uri. + proxyRequest.Headers.Host = null; + } + } +} diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppResourcesRequestTransformer.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppResourcesRequestTransformer.cs new file mode 100644 index 000000000..57373cf1e --- /dev/null +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/AssetProxy/FusionAppResourcesRequestTransformer.cs @@ -0,0 +1,45 @@ +using Equinor.ProjectExecutionPortal.ClientBackend.Configurations; +using Fusion; +using Microsoft.Extensions.Options; +using Microsoft.Identity.Web; +using Yarp.ReverseProxy.Forwarder; +using Yarp.ReverseProxy.Transforms; + +namespace Equinor.ProjectExecutionPortal.ClientBackend.AssetProxy +{ + public class FusionAppResourcesRequestTransformer : HttpTransformer + { + private readonly ITokenAcquisition _tokenAcquisition; + private readonly AssetProxyOptions _options; + + public FusionAppResourcesRequestTransformer(ITokenAcquisition tokenAcquisition, IOptions options) + { + _tokenAcquisition = tokenAcquisition; + _options = options.Value; + } + + public override async ValueTask TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) + { + var appIdentifier = httpContext.Request.RouteValues["appIdentifier"]; + var resourcePath = httpContext.Request.RouteValues["resourcePath"]; + + //var token = await tokenProvider.GetAccessTokenForUserAsync(new[] { options.TokenScope! }, user: httpContext.User); + var token = await _tokenAcquisition.GetAccessTokenForAppAsync(_options.TokenScope!); + + // Copy all request headers + await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); + + // Customize the query string: + var queryContext = new QueryTransformContext(httpContext.Request); + + // Assign the custom uri. Be careful about extra slashes when concatenating here. RequestUtilities.MakeDestinationAddress is a safe default. + proxyRequest.RequestUri = RequestUtilities.MakeDestinationAddress(_options.FusionAppsUrl!, $"/apps/{appIdentifier}", queryContext.QueryString); + + proxyRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + proxyRequest.Headers.Add("X-Fusion-App-Bundle-UniqueId", $"{httpContext.User.GetAzureUniqueId()}"); + + // Suppress the original request header, use the one from the destination Uri. + proxyRequest.Headers.Host = null; + } + } +} diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/AssetProxyOptions.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/AssetProxyOptions.cs index 60edec107..5bec9f2a5 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/AssetProxyOptions.cs +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/AssetProxyOptions.cs @@ -4,6 +4,7 @@ public class AssetProxyOptions { public string? FusionPortalUrl { get; set; } public string? FusionPeopleUrl { get; set; } + public string? FusionAppsUrl { get; set; } public string? TokenScope { get; set; } } } diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/FusionAppsApiOptions.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/FusionAppsApiOptions.cs new file mode 100644 index 000000000..7627f58fa --- /dev/null +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Configurations/FusionAppsApiOptions.cs @@ -0,0 +1,9 @@ +namespace Equinor.ProjectExecutionPortal.ClientBackend.Configurations +{ + public class FusionAppsApiOptions + { + public string? BaseAddress { get; set; } + public string? Scope { get; set; } + public string? ApiVersion { get; set; } + } +} diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Constants.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Constants.cs index 6c8cce21f..93fae4b32 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Constants.cs +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Constants.cs @@ -3,5 +3,6 @@ public class Constants { public const string HttpClientPortal = "fusion-portal"; + public const string FusionAppsRoute = "/fusion-apps"; } } diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Equinor.ProjectExecutionPortal.ClientBackend.csproj b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Equinor.ProjectExecutionPortal.ClientBackend.csproj index 5ed1864f8..fcc1de365 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Equinor.ProjectExecutionPortal.ClientBackend.csproj +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Equinor.ProjectExecutionPortal.ClientBackend.csproj @@ -9,7 +9,7 @@ - + diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Modules/AssetProxyConfiguration.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Modules/AssetProxyConfiguration.cs index 8efa162cb..ea9ef046a 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Modules/AssetProxyConfiguration.cs +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Modules/AssetProxyConfiguration.cs @@ -12,6 +12,8 @@ public static IServiceCollection AddFusionPortalAssetProxy(this IServiceCollecti .Configure("AssetProxy", configuration); services.AddScoped(); + //services.AddScoped(); + services.AddScoped(); services.AddScoped(); return services; @@ -19,10 +21,20 @@ public static IServiceCollection AddFusionPortalAssetProxy(this IServiceCollecti public static IEndpointRouteBuilder MapFusionPortalAssetProxy(this IEndpointRouteBuilder endpoints) { - endpoints.MapGet("/bundles/apps/{appKey}/resources/{*resourcePath}", AssetProxyHandler.ProxyRequestAsync); + // TO BE REMOVED ENDPOINTS + //endpoints.MapGet("/bundles/apps/{appKey}/resources/{*resourcePath}", AssetProxyHandler.ProxyRequestAsync); + + // ENDPOINTS TO KEEP endpoints.MapGet("/assets/images/profiles/{uniqueId}", AssetProxyHandler.ProxyRequestAsync); endpoints.MapGet("/images/profiles/{uniqueId}", AssetProxyHandler.ProxyRequestAsync); + // NEW ENDPOINTS + //endpoints.MapGet("/api/apps/{appIdentifier}", AssetProxyHandler.ProxyRequestAsync); + endpoints.MapGet($"{Constants.FusionAppsRoute}/{{**catch-all}}", AssetProxyHandler.ProxyRequestAsync); + //endpoints.MapGet("/api/apps/{appIdentifier}/builds/{versionIdentifier}/config", AssetProxyHandler.ProxyRequestAsync); + //endpoints.MapGet("/api/bundles/apps/{appIdentifier}/{versionIdentifier}", AssetProxyHandler.ProxyRequestAsync); + //endpoints.MapGet("/api/bundles/apps/{appIdentifier}/{versionIdentifier}/{resource}", AssetProxyHandler.ProxyRequestAsync); + return endpoints; } } diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Program.cs b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Program.cs index 4dd9ad8ac..bdfdd3779 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Program.cs +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Program.cs @@ -17,6 +17,7 @@ builder.Services.Configure(builder.Configuration.GetSection("ClientBundle")); builder.Services.Configure(builder.Configuration.GetSection("FusionBookmarks")); builder.Services.Configure(builder.Configuration.GetSection("FusionPortalApi")); +builder.Services.Configure(builder.Configuration.GetSection("FusionAppsApi")); builder.Services.Configure(builder.Configuration.GetSection("FusionProjectPortalApi")); builder.Services.Configure(builder.Configuration.GetSection("AssetProxy")); builder.Services.Configure(builder.Configuration.GetSection("ApplicationInsights")); @@ -37,12 +38,12 @@ // Add fusion integration builder.Services.AddFusionIntegration(fusionIntegrationConfig => { - var environment = builder.Configuration.GetValue("Fusion:Environment" ?? "ci"); + var environment = builder.Configuration.GetValue("Fusion:Environment") ?? "ci"; fusionIntegrationConfig.UseServiceInformation("Fusion.Project.Portal", environment); fusionIntegrationConfig.UseDefaultEndpointResolver(environment); fusionIntegrationConfig.UseDefaultTokenProvider(opts => { - opts.ClientId = builder.Configuration.GetValue("AzureAd:ClientId"); + opts.ClientId = builder.Configuration.GetValue("AzureAd:ClientId")!; opts.ClientSecret = builder.Configuration.GetValue("AzureAd:ClientSecret"); }); fusionIntegrationConfig.DisableClaimsTransformation(); diff --git a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Views/Bundle/Index.cshtml b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Views/Bundle/Index.cshtml index 97ebcb251..ee942a3df 100644 --- a/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Views/Bundle/Index.cshtml +++ b/clientBackend/src/Equinor.ProjectExecutionPortal.ClientBackend/Views/Bundle/Index.cshtml @@ -3,6 +3,7 @@ @using Microsoft.Extensions.Options @inject IOptions ClientBundleOptions; @inject IOptions FusionPortalApiOptions; +@inject IOptions FusionAppsApiOptions; @inject IOptions FusionProjectPortalApiOptions; @inject IOptions FusionBookmarksOptions; @inject IOptions ApplicationInsightsOptions; @@ -11,6 +12,7 @@ @{ var clientBundle = ClientBundleOptions.Value; var fusionPortalApi = FusionPortalApiOptions.Value; + var fusionAppsApi = FusionAppsApiOptions.Value; var fusionProjectPortalApi = FusionProjectPortalApiOptions.Value; var fusionBookmarks = FusionBookmarksOptions.Value; var applicationInsights = ApplicationInsightsOptions.Value; @@ -21,7 +23,6 @@ - @section Headers {