Skip to content

Commit a7453c1

Browse files
github-actions[bot]Adam RatzmandanmoseleyJamesNK
authored
[release/9.1] Hide secrets in source tooltip, correct ExpressionResolver logic for non-containers (#7708)
* Replace tuple with record type in source view model, hide secrets in tooltip * Remove sourceIsContainer checks in ResolveInternalAsync * Fix test * EndpointReference/Expression need to only be container * Apply suggestion * special case ResourceWithConnectionStringSurrogate * Update comment * Fix discrepant behavior between ReferenceExpression.GetValueAsync and ExpressionResolver.EvalExpressionAsync * Invoke GetValueAsync on ConnectionStringReference before resolving it, to prevent non-optional but missing value from resolving * Update src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs Co-authored-by: James Newton-King <[email protected]> * Improve comment in ResolveConnectionStringReferenceAsync * Change DashboardUIHelpers.GetMaskingText to return a record * clean up ResourceSourceViewModel * add additional secrets in test case * Add additional ExpressionResolver tests --------- Co-authored-by: Adam Ratzman <[email protected]> Co-authored-by: Dan Moseley <[email protected]> Co-authored-by: James Newton-King <[email protected]>
1 parent 87cb282 commit a7453c1

File tree

8 files changed

+162
-77
lines changed

8 files changed

+162
-77
lines changed

src/Aspire.Dashboard/Components/Controls/GridValue.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@if (EnableMasking && IsMasked)
1111
{
1212
<span class="grid-value masked" id="@_cellTextId">
13-
@DashboardUIHelpers.GetMaskingText(length: 8)
13+
@DashboardUIHelpers.GetMaskingText(length: 8).MarkupString
1414
</span>
1515
}
1616
else
@@ -32,7 +32,7 @@
3232
@((MarkupString)_formattedValue)
3333
}
3434
@ContentAfterValue
35-
}
35+
}
3636
</span>
3737
}
3838

src/Aspire.Dashboard/Components/ResourcesGridColumns/SourceColumnDisplay.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525
else
2626
{
27-
<span class="subtext">&nbsp;@DashboardUIHelpers.GetMaskingText(length: 6)</span>
27+
<span class="subtext">&nbsp;@DashboardUIHelpers.GetMaskingText(length: 6).MarkupString</span>
2828
}
2929
}
3030
}
Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using Aspire.Dashboard.Utils;
5+
46
namespace Aspire.Dashboard.Model;
57

68
public class ResourceSourceViewModel(string value, List<LaunchArgument>? contentAfterValue, string valueToVisualize, string tooltip)
@@ -12,65 +14,22 @@ public class ResourceSourceViewModel(string value, List<LaunchArgument>? content
1214

1315
internal static ResourceSourceViewModel? GetSourceViewModel(ResourceViewModel resource)
1416
{
15-
(List<LaunchArgument>? Arguments, string? ArgumentsString) commandLineInfo;
16-
17-
// If the resource contains launch arguments, these project arguments should be shown in place of all executable arguments,
18-
// which include args added by the app host
19-
if (resource.TryGetAppArgs(out var launchArguments))
20-
{
21-
if (launchArguments.IsDefaultOrEmpty)
22-
{
23-
commandLineInfo = (null, null);
24-
}
25-
else
26-
{
27-
var argumentsString = string.Join(" ", launchArguments);
28-
if (resource.TryGetAppArgsSensitivity(out var areArgumentsSensitive))
29-
{
30-
var arguments = launchArguments
31-
.Select((arg, i) => new LaunchArgument(arg, IsShown: !areArgumentsSensitive[i]))
32-
.ToList();
33-
34-
commandLineInfo = (Arguments: arguments, argumentsString);
35-
}
36-
else
37-
{
38-
commandLineInfo = (Arguments: launchArguments.Select(arg => new LaunchArgument(arg, true)).ToList(), argumentsString);
39-
}
40-
}
41-
}
42-
else if (resource.TryGetExecutableArguments(out var executableArguments) && !resource.IsProject())
43-
{
44-
var arguments = executableArguments.IsDefaultOrEmpty ? null : executableArguments.Select(arg => new LaunchArgument(arg, true)).ToList();
45-
commandLineInfo = (Arguments: arguments, string.Join(' ', executableArguments));
46-
}
47-
else
48-
{
49-
commandLineInfo = (Arguments: null, null);
50-
}
17+
var commandLineInfo = GetCommandLineInfo(resource);
5118

5219
// NOTE projects are also executables, so we have to check for projects first
5320
if (resource.IsProject() && resource.TryGetProjectPath(out var projectPath))
5421
{
55-
if (commandLineInfo is { Arguments: { } arguments, ArgumentsString: { } fullCommandLine })
56-
{
57-
return new ResourceSourceViewModel(value: Path.GetFileName(projectPath), contentAfterValue: arguments, valueToVisualize: $"{projectPath} {fullCommandLine}", tooltip: $"{projectPath} {fullCommandLine}");
58-
}
59-
60-
// default to project path if there is no executable path or executable arguments
61-
return new ResourceSourceViewModel(value: Path.GetFileName(projectPath), contentAfterValue: commandLineInfo.Arguments, valueToVisualize: projectPath, tooltip: projectPath);
22+
return CreateResourceSourceViewModel(Path.GetFileName(projectPath), projectPath, commandLineInfo);
6223
}
6324

6425
if (resource.TryGetExecutablePath(out var executablePath))
6526
{
66-
var fullSource = commandLineInfo.ArgumentsString is not null ? $"{executablePath} {commandLineInfo.ArgumentsString}" : executablePath;
67-
return new ResourceSourceViewModel(value: Path.GetFileName(executablePath), contentAfterValue: commandLineInfo.Arguments, valueToVisualize: fullSource, tooltip: fullSource);
27+
return CreateResourceSourceViewModel(Path.GetFileName(executablePath), executablePath, commandLineInfo);
6828
}
6929

7030
if (resource.TryGetContainerImage(out var containerImage))
7131
{
72-
var fullSource = commandLineInfo.ArgumentsString is null ? containerImage : $"{containerImage} {commandLineInfo.ArgumentsString}";
73-
return new ResourceSourceViewModel(value: containerImage, contentAfterValue: commandLineInfo.Arguments, valueToVisualize: fullSource, tooltip: fullSource);
32+
return CreateResourceSourceViewModel(containerImage, containerImage, commandLineInfo);
7433
}
7534

7635
if (resource.Properties.TryGetValue(KnownProperties.Resource.Source, out var property) && property.Value is { HasStringValue: true, StringValue: var value })
@@ -79,7 +38,56 @@ public class ResourceSourceViewModel(string value, List<LaunchArgument>? content
7938
}
8039

8140
return null;
41+
42+
static CommandLineInfo? GetCommandLineInfo(ResourceViewModel resourceViewModel)
43+
{
44+
// If the resource contains launch arguments, these project arguments should be shown in place of all executable arguments,
45+
// which include args added by the app host
46+
if (resourceViewModel.TryGetAppArgs(out var launchArguments))
47+
{
48+
if (launchArguments.IsDefaultOrEmpty)
49+
{
50+
return null;
51+
}
52+
53+
var argumentsString = string.Join(" ", launchArguments);
54+
if (resourceViewModel.TryGetAppArgsSensitivity(out var areArgumentsSensitive))
55+
{
56+
var arguments = launchArguments
57+
.Select((arg, i) => new LaunchArgument(arg, IsShown: !areArgumentsSensitive[i]))
58+
.ToList();
59+
60+
return new CommandLineInfo(
61+
Arguments: arguments,
62+
ArgumentsString: argumentsString,
63+
TooltipString: string.Join(" ", arguments.Select(arg => arg.IsShown
64+
? arg.Value
65+
: DashboardUIHelpers.GetMaskingText(6).Text)));
66+
}
67+
68+
return new CommandLineInfo(Arguments: launchArguments.Select(arg => new LaunchArgument(arg, true)).ToList(), ArgumentsString: argumentsString, TooltipString: argumentsString);
69+
}
70+
71+
if (resourceViewModel.TryGetExecutableArguments(out var executableArguments) && !resourceViewModel.IsProject())
72+
{
73+
var arguments = executableArguments.IsDefaultOrEmpty ? [] : executableArguments.Select(arg => new LaunchArgument(arg, true)).ToList();
74+
var argumentsString = string.Join(" ", executableArguments);
75+
76+
return new CommandLineInfo(Arguments: arguments, ArgumentsString: argumentsString, TooltipString: argumentsString);
77+
}
78+
79+
return null;
80+
}
81+
82+
static ResourceSourceViewModel CreateResourceSourceViewModel(string value, string path, CommandLineInfo? commandLineInfo)
83+
{
84+
return commandLineInfo is not null
85+
? new ResourceSourceViewModel(value: value, contentAfterValue: commandLineInfo.Arguments, valueToVisualize: $"{path} {commandLineInfo.ArgumentsString}", tooltip: $"{path} {commandLineInfo.TooltipString}")
86+
: new ResourceSourceViewModel(value: value, contentAfterValue: null, valueToVisualize: path, tooltip: path);
87+
}
8288
}
89+
90+
private record CommandLineInfo(List<LaunchArgument> Arguments, string ArgumentsString, string TooltipString);
8391
}
8492

8593
public record LaunchArgument(string Value, bool IsShown);

src/Aspire.Dashboard/Utils/DashboardUIHelpers.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,25 @@ public static (ColumnResizeLabels resizeLabels, ColumnSortLabels sortLabels) Cre
4343
return (resizeLabels, sortLabels);
4444
}
4545

46-
private static readonly ConcurrentDictionary<int, string> s_cachedMasking = new();
46+
private static readonly ConcurrentDictionary<int, TextMask> s_cachedMasking = new();
4747

48-
public static MarkupString GetMaskingText(int length)
48+
public static TextMask GetMaskingText(int length)
4949
{
50-
return new MarkupString(s_cachedMasking.GetOrAdd(length, static i =>
50+
return s_cachedMasking.GetOrAdd(length, static i =>
5151
{
52-
const string maskingChar = "&#x25cf;";
53-
return new StringBuilder(maskingChar.Length * i)
54-
.Insert(0, maskingChar, i)
55-
.ToString();
56-
}));
52+
const string markupMaskingChar = "&#x25cf;";
53+
const string textMaskingChar = "●";
54+
55+
return new TextMask(
56+
new MarkupString(Repeat(markupMaskingChar, i)),
57+
Repeat(textMaskingChar, i)
58+
);
59+
60+
static string Repeat(string s, int n) => new StringBuilder(s.Length * n)
61+
.Insert(0, s, n)
62+
.ToString();
63+
});
5764
}
5865
}
66+
67+
internal record TextMask(MarkupString MarkupString, string Text);

src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr)
9494
}
9595
}
9696

97-
return new(string.Format(CultureInfo.InvariantCulture, expr.Format, args), isSensitive);
97+
// Identically to ReferenceExpression.GetValueAsync, we return null if the format is empty
98+
var value = expr.Format.Length == 0 ? null : string.Format(CultureInfo.InvariantCulture, expr.Format, args);
99+
100+
return new ResolvedValue(value, isSensitive);
98101
}
99102

100103
async Task<ResolvedValue> EvalValueProvider(IValueProvider vp)
@@ -148,22 +151,29 @@ async Task<ResolvedValue> EvalValueProvider(IValueProvider vp)
148151
return new ResolvedValue(value, false);
149152
}
150153

154+
async Task<ResolvedValue> ResolveConnectionStringReferenceAsync(ConnectionStringReference cs)
155+
{
156+
// We are substituting our own logic for ConnectionStringReference's GetValueAsync.
157+
// However, ConnectionStringReference#GetValueAsync will throw if the connection string is not optional but is not present.
158+
// To avoid duplicating that logic, we can defer to ConnectionStringReference#GetValueAsync, which will throw if needed.
159+
await ((IValueProvider)cs).GetValueAsync(cancellationToken).ConfigureAwait(false);
160+
161+
return await ResolveInternalAsync(cs.Resource.ConnectionStringExpression).ConfigureAwait(false);
162+
}
163+
151164
/// <summary>
152-
/// Resolve an expression when it is being used from inside a container.
153-
/// So it's either a container-to-container or container-to-exe communication.
165+
/// Resolve an expression. When it is being used from inside a container, endpoints may be evaluated (either in a container-to-container or container-to-exe communication).
154166
/// </summary>
155167
async ValueTask<ResolvedValue> ResolveInternalAsync(object? value)
156168
{
157-
return (value, sourceIsContainer) switch
169+
return value switch
158170
{
159-
(ConnectionStringReference cs, true) => await ResolveInternalAsync(cs.Resource.ConnectionStringExpression).ConfigureAwait(false),
160-
(IResourceWithConnectionString cs, true) => await ResolveInternalAsync(cs.ConnectionStringExpression).ConfigureAwait(false),
161-
(ReferenceExpression ex, false) => await EvalExpressionAsync(ex).ConfigureAwait(false),
162-
(ReferenceExpression ex, true) => await EvalExpressionAsync(ex).ConfigureAwait(false),
163-
(EndpointReference endpointReference, true) => new(await EvalEndpointAsync(endpointReference, EndpointProperty.Url).ConfigureAwait(false), false),
164-
(EndpointReferenceExpression ep, true) => new(await EvalEndpointAsync(ep.Endpoint, ep.Property).ConfigureAwait(false), false),
165-
(IValueProvider vp, false) => await EvalValueProvider(vp).ConfigureAwait(false),
166-
(IValueProvider vp, true) => await EvalValueProvider(vp).ConfigureAwait(false),
171+
ConnectionStringReference cs => await ResolveConnectionStringReferenceAsync(cs).ConfigureAwait(false),
172+
IResourceWithConnectionString cs and not ResourceWithConnectionStringSurrogate => await ResolveInternalAsync(cs.ConnectionStringExpression).ConfigureAwait(false),
173+
ReferenceExpression ex => await EvalExpressionAsync(ex).ConfigureAwait(false),
174+
EndpointReference endpointReference when sourceIsContainer => new ResolvedValue(await EvalEndpointAsync(endpointReference, EndpointProperty.Url).ConfigureAwait(false), false),
175+
EndpointReferenceExpression ep when sourceIsContainer => new ResolvedValue(await EvalEndpointAsync(ep.Endpoint, ep.Property).ConfigureAwait(false), false),
176+
IValueProvider vp => await EvalValueProvider(vp).ConfigureAwait(false),
167177
_ => throw new NotImplementedException()
168178
};
169179
}

src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ private ReferenceExpression(string format, IValueProvider[] valueProviders, stri
5555
/// <returns></returns>
5656
public async ValueTask<string?> GetValueAsync(CancellationToken cancellationToken)
5757
{
58+
// NOTE: any logical changes to this method should also be made to ExpressionResolver.EvalExpressionAsync
5859
if (Format.Length == 0)
5960
{
6061
return null;

tests/Aspire.Dashboard.Tests/Model/ResourceSourceViewModelTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Dashboard.Model;
5+
using Aspire.Dashboard.Utils;
56
using Aspire.Tests.Shared.DashboardModel;
67
using Google.Protobuf.WellKnownTypes;
78
using Microsoft.Extensions.Logging.Abstractions;
@@ -80,21 +81,22 @@ void AddStringProperty(string propertyName, string? propertyValue)
8081
valueToVisualize: "path/to/project arg2",
8182
tooltip: "path/to/project arg2"));
8283

84+
var maskingText = DashboardUIHelpers.GetMaskingText(6).Text;
8385
// Project with app arguments, as well as a secret (format argument)
8486
data.Add(new TestData(
8587
ResourceType: "Project",
8688
ExecutablePath: "path/to/executable",
8789
ExecutableArguments: ["arg1", "arg2"],
88-
AppArgs: ["arg2", "--key", "secret"],
89-
AppArgsSensitivity: [false, false, true],
90+
AppArgs: ["arg2", "--key", "secret", "secret2", "notsecret"],
91+
AppArgsSensitivity: [false, false, true, true, false],
9092
ProjectPath: "path/to/project",
9193
ContainerImage: null,
9294
SourceProperty: null),
9395
new ResourceSourceViewModel(
9496
value: "project",
95-
contentAfterValue: [new LaunchArgument("arg2", true), new LaunchArgument("--key", true), new LaunchArgument("secret", false)],
96-
valueToVisualize: "path/to/project arg2 --key secret",
97-
tooltip: "path/to/project arg2 --key secret"));
97+
contentAfterValue: [new LaunchArgument("arg2", true), new LaunchArgument("--key", true), new LaunchArgument("secret", false), new LaunchArgument("secret2", false), new LaunchArgument("notsecret", true)],
98+
valueToVisualize: "path/to/project arg2 --key secret secret2 notsecret",
99+
tooltip: $"path/to/project arg2 --key {maskingText} {maskingText} notsecret"));
98100

99101
// Project without executable arguments
100102
data.Add(new TestData(

0 commit comments

Comments
 (0)