Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default to browser token auth in dashboard standalone #3427

Merged
merged 1 commit into from
Apr 8, 2024
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
3 changes: 3 additions & 0 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
</FluentButton>
</FluentHeader>
<NavMenu />
<div class="messagebar-container">
<FluentMessageBarProvider Section="@MessageBarSection" Class="top-messagebar" />
</div>
<FluentBodyContent Class="custom-body-content">
<FluentToastProvider />
@Body
Expand Down
29 changes: 29 additions & 0 deletions src/Aspire.Dashboard/Components/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Utils;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.JSInterop;

Expand All @@ -22,6 +24,7 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable

private const string SettingsDialogId = "SettingsDialog";
private const string HelpDialogId = "HelpDialog";
private const string MessageBarSection = "MessagesTop";

[Inject]
public required ThemeManager ThemeManager { get; init; }
Expand All @@ -47,6 +50,12 @@ public partial class MainLayout : IGlobalKeydownListener, IAsyncDisposable
[Inject]
public required ShortcutManager ShortcutManager { get; init; }

[Inject]
public required IMessageService MessageService { get; init; }

[Inject]
public required IOptionsMonitor<DashboardOptions> Options { get; init; }

protected override async Task OnInitializedAsync()
{
// Theme change can be triggered from the settings dialog. This logic applies the new theme to the browser window.
Expand Down Expand Up @@ -78,6 +87,26 @@ protected override async Task OnInitializedAsync()

var result = await JS.InvokeAsync<string>("window.getBrowserTimeZone");
TimeProvider.SetBrowserTimeZone(result);

if (Options.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured)
{
// ShowMessageBarAsync must come after an await. Otherwise it will NRE.
// I think this order allows the message bar provider to be fully initialized.
await MessageService.ShowMessageBarAsync(options =>
{
options.Title = Loc[nameof(Resources.Layout.MessageTelemetryTitle)];
options.Body = Loc[nameof(Resources.Layout.MessageTelemetryBody)];
options.Link = new()
{
Text = Loc[nameof(Resources.Layout.MessageTelemetryLink)],
Href = "https://aka.ms/dotnet/aspire/telemetry-unsecured",
Target = "_blank"
};
options.Intent = MessageIntent.Warning;
options.Section = MessageBarSection;
options.AllowDismiss = true;
});
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
Expand Down
8 changes: 7 additions & 1 deletion src/Aspire.Dashboard/Components/Layout/MainLayout.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@
width: 100vw;
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
grid-template-rows: auto auto 1fr;
grid-template-areas:
"icon head"
"nav messagebar"
"nav main";
background-color: var(--fill-color);
color: var(--neutral-foreground-rest);
Expand All @@ -60,6 +61,11 @@
::deep.layout > .body-content {
grid-area: main;
overflow-x: auto; /* allow horizontal scrolling */
border-left: 1px solid var(--neutral-stroke-rest);
}

::deep.layout > .messagebar-container {
grid-area: messagebar;
border-top: 1px solid var(--neutral-stroke-rest);
border-left: 1px solid var(--neutral-stroke-rest);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Configuration/DashboardOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Aspire.Hosting;

namespace Aspire.Dashboard.Configuration;

Expand Down Expand Up @@ -84,7 +85,7 @@ internal bool TryParseOptions([NotNullWhen(false)] out string? errorMessage)
{
if (string.IsNullOrEmpty(EndpointUrl))
{
errorMessage = "OTLP endpoint URL is not configured. Specify a Dashboard:Otlp:EndpointUrl value.";
errorMessage = $"OTLP endpoint URL is not configured. Specify a {DashboardConfigNames.DashboardOtlpUrlName.EnvVarName} value.";
return false;
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,19 @@ public void PostConfigure(string? name, DashboardOptions options)
options.Frontend.AuthMode = FrontendAuthMode.Unsecured;
options.Otlp.AuthMode = OtlpAuthMode.Unsecured;
}
else
{
options.Frontend.AuthMode ??= FrontendAuthMode.BrowserToken;
options.Otlp.AuthMode ??= OtlpAuthMode.Unsecured;
}
if (options.Frontend.AuthMode == FrontendAuthMode.BrowserToken && string.IsNullOrEmpty(options.Frontend.BrowserToken))
{
options.Frontend.BrowserToken = TokenGenerator.GenerateToken();
var token = TokenGenerator.GenerateToken();

// Set the generated token in configuration. This is required because options could be created multiple times
// (at startup, after CI is created, after options change). Setting the token in configuration makes it consistent.
_configuration[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = token;
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
options.Frontend.BrowserToken = token;
}
}
}
29 changes: 20 additions & 9 deletions src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Reflection;
using System.Security.Claims;
Expand All @@ -20,6 +21,7 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Server.Kestrel;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -165,10 +167,10 @@ public DashboardWebApplication(Action<WebApplicationBuilder>? configureBuilder =
{
if (_frontendEndPointAccessor != null)
{
var url = GetEndpointUrl(_frontendEndPointAccessor());
var url = _frontendEndPointAccessor().Address;
_logger.LogInformation("Now listening on: {DashboardUri}", url);

var options = _app.Services.GetRequiredService<IOptions<DashboardOptions>>().Value;
var options = _app.Services.GetRequiredService<IOptionsMonitor<DashboardOptions>>().CurrentValue;
if (options.Frontend.AuthMode == FrontendAuthMode.BrowserToken)
{
LoggingHelpers.WriteDashboardUrl(_logger, url, options.Frontend.BrowserToken);
Expand All @@ -178,10 +180,13 @@ public DashboardWebApplication(Action<WebApplicationBuilder>? configureBuilder =
if (_otlpServiceEndPointAccessor != null)
{
// This isn't used by dotnet watch but still useful to have for debugging
_logger.LogInformation("OTLP server running at: {OtlpEndpointUri}", GetEndpointUrl(_otlpServiceEndPointAccessor()));
_logger.LogInformation("OTLP server running at: {OtlpEndpointUri}", _otlpServiceEndPointAccessor().Address);
}

static string GetEndpointUrl(EndpointInfo info) => $"{(info.isHttps ? "https" : "http")}://{info.EndPoint}";
if (_dashboardOptionsMonitor.CurrentValue.Otlp.AuthMode == OtlpAuthMode.Unsecured)
{
_logger.LogWarning("OTLP server is unsecured. Untrusted apps can send telemetry to the dashboard. For more information, visit https://go.microsoft.com/fwlink/?linkid=2267030");
}
});

// Redirect browser directly to /structuredlogs address if the dashboard is running without a resource service.
Expand Down Expand Up @@ -383,13 +388,13 @@ static void AddEndpointConfiguration(Dictionary<string, string?> values, string
{
// Only the last endpoint is accessible. Tests should only need one but
// this will need to be improved if that changes.
_frontendEndPointAccessor = CreateEndPointAccessor(endpointConfiguration.ListenOptions, endpointConfiguration.IsHttps);
_frontendEndPointAccessor ??= CreateEndPointAccessor(endpointConfiguration);
});
}

configurationLoader.Endpoint("Otlp", endpointConfiguration =>
{
_otlpServiceEndPointAccessor = CreateEndPointAccessor(endpointConfiguration.ListenOptions, endpointConfiguration.IsHttps);
_otlpServiceEndPointAccessor ??= CreateEndPointAccessor(endpointConfiguration);
if (hasSingleEndpoint)
{
logger.LogDebug("Browser and OTLP accessible on a single endpoint.");
Expand Down Expand Up @@ -417,14 +422,20 @@ static void AddEndpointConfiguration(Dictionary<string, string?> values, string
});
});

static Func<EndpointInfo> CreateEndPointAccessor(ListenOptions options, bool isHttps)
static Func<EndpointInfo> CreateEndPointAccessor(EndpointConfiguration endpointConfiguration)
{
// We want to provide a way for someone to get the IP address of an endpoint.
// However, if a dynamic port is used, the port is not known until the server is started.
// Instead of returning the ListenOption's endpoint directly, we provide a func that returns the endpoint.
// The endpoint on ListenOptions is updated after binding, so accessing it via the func after the server
// has started returns the resolved port.
return () => new EndpointInfo(options.IPEndPoint!, isHttps);
var address = BindingAddress.Parse(endpointConfiguration.ConfigSection["Url"]!);
return () =>
{
var endpoint = endpointConfiguration.ListenOptions.IPEndPoint!;
var resolvedAddress = address.Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + address.Host.ToLowerInvariant() + ":" + endpoint.Port.ToString(CultureInfo.InvariantCulture);
return new EndpointInfo(resolvedAddress, endpoint, endpointConfiguration.IsHttps);
};
}
}

Expand Down Expand Up @@ -601,7 +612,7 @@ public static class FrontendAuthenticationDefaults
}
}

public record EndpointInfo(IPEndPoint EndPoint, bool isHttps);
public record EndpointInfo(string Address, IPEndPoint EndPoint, bool isHttps);

public static class FrontendAuthorizationDefaults
{
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Dashboard/Model/BrowserTimeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Aspire.Dashboard.Model;

/// <summary>
/// This time provider is used to provide the time zone information from the browser to the server.
/// It is a different type because we want to log setting the timezone, and we want a distinct type
/// It is a different type because we want to log setting the time zone, and we want a distinct type
/// to register with DI:
/// - BrowserTimeProvider must be scoped to the user's session.
/// - The built-in TimeProvider registration must be singleton for the system time (used by auth).
Expand All @@ -29,11 +29,11 @@ public void SetBrowserTimeZone(string timeZone)
{
if (!TimeZoneInfo.TryFindSystemTimeZoneById(timeZone, out var timeZoneInfo))
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
{
_logger.LogWarning("Couldn't find a time zone '{TimeZone}'. Defaulting to UTC.", timeZone);
_logger.LogWarning("Couldn't find time zone '{TimeZone}'. Defaulting to UTC.", timeZone);
timeZoneInfo = TimeZoneInfo.Utc;
}

_logger.LogInformation("Browser time zone set to '{TimeZone}' with UTC offset {UtcOffset}.", timeZoneInfo.Id, timeZoneInfo.BaseUtcOffset);
_logger.LogDebug("Browser time zone set to '{TimeZone}' with UTC offset {UtcOffset}.", timeZoneInfo.Id, timeZoneInfo.BaseUtcOffset);
_browserLocalTimeZone = timeZoneInfo;
}
}
7 changes: 6 additions & 1 deletion src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ public async Task InvokeAsync(HttpContext context)
var qs = HttpUtility.ParseQueryString(context.Request.QueryString.ToString());
qs.Remove("t");

// Collection created by ParseQueryString handles escaping names and values.
var newQuerystring = qs.ToString();
context.Response.Redirect($"{context.Request.Path}?{newQuerystring}");
if (!string.IsNullOrEmpty(newQuerystring))
{
newQuerystring = "?" + newQuerystring;
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
}
context.Response.Redirect($"{context.Request.Path}{newQuerystring}");
}

return;
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Dashboard/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:15887"
},
"applicationUrl": "https://localhost:15889;http://localhost:15888"
}
Expand Down
27 changes: 27 additions & 0 deletions src/Aspire.Dashboard/Resources/Layout.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/Aspire.Dashboard/Resources/Layout.resx
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,13 @@
<data name="MainLayoutAspire" xml:space="preserve">
<value>.NET Aspire</value>
</data>
<data name="MessageTelemetryBody" xml:space="preserve">
<value>Untrusted apps can send telemetry to the dashboard.</value>
</data>
<data name="MessageTelemetryLink" xml:space="preserve">
<value>More information</value>
</data>
<data name="MessageTelemetryTitle" xml:space="preserve">
<value>Telemetry endpoint is unsecured</value>
</data>
</root>
15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Layout.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Layout.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions src/Aspire.Dashboard/Resources/xlf/Layout.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading