Skip to content

Commit 261532e

Browse files
authored
Add server validation of interaction inputs (#10527)
1 parent ec04c1a commit 261532e

File tree

10 files changed

+284
-23
lines changed

10 files changed

+284
-23
lines changed

playground/Stress/Stress.AppHost/InteractionCommands.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ public static IResourceBuilder<T> AddInteractionCommands<T>(this IResourceBuilde
127127
var numberOfPeopleInput = new InteractionInput { InputType = InputType.Number, Label = "Number of people", Placeholder = "Enter number of people", Value = "2", Required = true };
128128
var inputs = new List<InteractionInput>
129129
{
130-
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true },
131-
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true },
130+
new InteractionInput { InputType = InputType.Text, Label = "Name", Placeholder = "Enter name", Required = true, MaxLength = 50 },
131+
new InteractionInput { InputType = InputType.SecretText, Label = "Password", Placeholder = "Enter password", Required = true, MaxLength = 20 },
132132
dinnerInput,
133133
numberOfPeopleInput,
134134
new InteractionInput { InputType = InputType.Boolean, Label = "Remember me", Placeholder = "What does this do?", Required = true },

src/Aspire.Dashboard/Aspire.Dashboard.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,8 @@
270270
<Compile Include="$(SharedDir)ConsoleLogs\LogEntry.cs" Link="Utils\ConsoleLogs\LogEntry.cs" />
271271
<Compile Include="$(SharedDir)ConsoleLogs\LogPauseViewModel.cs" Link="Utils\ConsoleLogs\LogPauseViewModel.cs" />
272272
<Compile Include="$(SharedDir)ConsoleLogs\TimestampParser.cs" Link="Utils\ConsoleLogs\TimestampParser.cs" />
273-
<Compile Include="..\Shared\KnownUrls.cs" Link="Utils\KnownUrls.cs" />
273+
<Compile Include="$(SharedDir)KnownUrls.cs" Link="Utils\KnownUrls.cs" />
274+
<Compile Include="$(SharedDir)InteractionHelpers.cs" Link="Utils\InteractionHelpers.cs" />
274275
</ItemGroup>
275276

276277
<ItemGroup>

src/Aspire.Dashboard/Components/Dialogs/InteractionsInputDialog.razor

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@
3333
<FluentStack Orientation="Orientation.Vertical" VerticalGap="12">
3434
@foreach (var vm in _inputDialogInputViewModels)
3535
{
36-
@*
37-
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
38-
* Immediate value of true on text inputs ensures the value is set to the server token with every key press in textbox.
39-
*@
4036
var localItem = vm;
4137
var descriptionId = !string.IsNullOrEmpty(localItem.Input.Description)
4238
? $"{_elementRefs[localItem]?.Id}-description"
@@ -45,17 +41,25 @@
4541
@switch (vm.Input.InputType)
4642
{
4743
case InputType.Text:
44+
@*
45+
* Immediate value of true on text input ensures the value is set to the server token with every key press in textbox.
46+
*@
4847
<FluentTextField @ref="@_elementRefs[localItem]"
4948
@bind-Value="localItem.Value"
5049
Label="@localItem.Input.Label"
5150
Placeholder="@localItem.Input.Placeholder"
5251
Required="localItem.Input.Required"
5352
Immediate="true"
54-
aria-describedby="@descriptionId" />
53+
aria-describedby="@descriptionId"
54+
Maxlength="@InteractionHelpers.GetMaxLength(localItem.Input.MaxLength)" />
5555
@GetDescriptionContent(localItem.Input, descriptionId)
5656
<ValidationMessage For="@(() => localItem.Value)" />
5757
break;
5858
case InputType.SecretText:
59+
@*
60+
* AutoComplete value of one-time-code on password input prevents the browser asking to save the value.
61+
* Immediate value of true on text input ensures the value is set to the server token with every key press in textbox.
62+
*@
5963
<FluentTextField @ref="@_elementRefs[localItem]"
6064
@bind-Value="localItem.Value"
6165
Label="@localItem.Input.Label"
@@ -64,7 +68,8 @@
6468
TextFieldType="TextFieldType.Password"
6569
AutoComplete="one-time-code"
6670
Immediate="true"
67-
aria-describedby="@descriptionId" />
71+
aria-describedby="@descriptionId"
72+
Maxlength="@InteractionHelpers.GetMaxLength(localItem.Input.MaxLength)" />
6873
@GetDescriptionContent(localItem.Input, descriptionId)
6974
<ValidationMessage For="@(() => localItem.Value)" />
7075
break;

src/Aspire.Hosting/Aspire.Hosting.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Compile Include="$(SharedDir)PortAllocator.cs" Link="Publishing\PortAllocator.cs" />
4343
<Compile Include="$(SharedDir)OverloadResolutionPriorityAttribute.cs" Link="Utils\OverloadResolutionPriorityAttribute.cs" />
4444
<Compile Include="$(SharedDir)PackageUpdateHelpers.cs" Link="Utils\PackageUpdateHelpers.cs" />
45+
<Compile Include="$(SharedDir)InteractionHelpers.cs" Link="Utils\InteractionHelpers.cs" />
4546
</ItemGroup>
4647

4748
<ItemGroup>

src/Aspire.Hosting/Dashboard/DashboardService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ async Task WatchInteractionsInternal(CancellationToken cancellationToken)
143143
{
144144
dto.Options.Add(input.Options.ToDictionary());
145145
}
146+
if (input.MaxLength != null)
147+
{
148+
dto.MaxLength = input.MaxLength.Value;
149+
}
146150
dto.ValidationErrors.AddRange(input.ValidationErrors);
147151
return dto;
148152
}).ToList();

src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ message InteractionInput {
376376
repeated string validation_errors = 7;
377377
string description = 8;
378378
bool enable_description_markdown = 9;
379+
int32 max_length = 10;
379380
}
380381
enum MessageIntent {
381382
MESSAGE_INTENT_NONE = 0;

src/Aspire.Hosting/IInteractionService.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ public sealed class InteractionInput
144144
/// </summary>
145145
public string? Placeholder { get; set; }
146146

147+
/// <summary>
148+
/// gets or sets the maximum length for text inputs.
149+
/// </summary>
150+
public int? MaxLength
151+
{
152+
get => field;
153+
set
154+
{
155+
if (value is { } v)
156+
{
157+
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(v, 0);
158+
}
159+
160+
field = value;
161+
}
162+
}
163+
147164
internal List<string> ValidationErrors { get; } = [];
148165
}
149166

src/Aspire.Hosting/InteractionService.cs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.ObjectModel;
55
using System.Diagnostics;
6+
using System.Globalization;
67
using System.Runtime.CompilerServices;
78
using System.Threading.Channels;
89
using Microsoft.Extensions.Logging;
@@ -234,25 +235,77 @@ private async Task<bool> RunValidationAsync(Interaction interactionState, Intera
234235
// State could be null if the user dismissed the inputs dialog. There is nothing to validate in this situation.
235236
if (result.State is IReadOnlyList<InteractionInput> inputs)
236237
{
237-
var options = (InputsDialogInteractionOptions)interactionState.Options;
238+
foreach (var input in inputs)
239+
{
240+
input.ValidationErrors.Clear();
241+
}
242+
243+
var context = new InputsDialogValidationContext
244+
{
245+
CancellationToken = cancellationToken,
246+
ServiceProvider = _serviceProvider,
247+
Inputs = inputsInfo.Inputs
248+
};
238249

239-
if (options.ValidationCallback is { } validationCallback)
250+
foreach (var input in inputs)
240251
{
241-
foreach (var input in inputs)
252+
var value = input.Value = input.Value?.Trim();
253+
254+
if (string.IsNullOrEmpty(value))
242255
{
243-
input.ValidationErrors.Clear();
256+
if (input.Required)
257+
{
258+
context.AddValidationError(input, "Value is required.");
259+
}
244260
}
245-
246-
var context = new InputsDialogValidationContext
261+
else
247262
{
248-
CancellationToken = cancellationToken,
249-
ServiceProvider = _serviceProvider,
250-
Inputs = inputsInfo.Inputs
251-
};
252-
await validationCallback(context).ConfigureAwait(false);
263+
switch (input.InputType)
264+
{
265+
case InputType.Text:
266+
case InputType.SecretText:
267+
var maxLength = InteractionHelpers.GetMaxLength(input.MaxLength);
268+
269+
if (value.Length > maxLength)
270+
{
271+
context.AddValidationError(input, $"Value length exceeds {maxLength} characters.");
272+
}
273+
break;
274+
case InputType.Choice:
275+
if (!input.Options?.Any(o => o.Key == value) ?? true)
276+
{
277+
context.AddValidationError(input, "Value must be one of the provided options.");
278+
}
279+
break;
280+
case InputType.Boolean:
281+
if (!bool.TryParse(value, out _))
282+
{
283+
context.AddValidationError(input, "Value must be a valid boolean.");
284+
}
285+
break;
286+
case InputType.Number:
287+
if (!int.TryParse(value, CultureInfo.InvariantCulture, out _))
288+
{
289+
context.AddValidationError(input, "Value must be a valid number.");
290+
}
291+
break;
292+
default:
293+
break;
294+
}
295+
}
296+
}
253297

254-
return !context.HasErrors;
298+
// Only run validation callback if there are no data validation errors.
299+
if (!context.HasErrors)
300+
{
301+
var options = (InputsDialogInteractionOptions)interactionState.Options;
302+
if (options.ValidationCallback is { } validationCallback)
303+
{
304+
await validationCallback(context).ConfigureAwait(false);
305+
}
255306
}
307+
308+
return !context.HasErrors;
256309
}
257310
}
258311

src/Shared/InteractionHelpers.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using System.Diagnostics;
6+
using System.Security.Cryptography;
7+
using System.Text;
8+
9+
namespace Aspire;
10+
11+
internal static class InteractionHelpers
12+
{
13+
// Chosen to balance between being long enough for most normal use but provides a default limit
14+
// to prevent possible abuse of interactions API.
15+
public const int DefaultMaxLength = 8000;
16+
17+
public static int GetMaxLength(int? configuredInputLength)
18+
{
19+
// An unconfigured max length uses the default.
20+
if (configuredInputLength is null || configuredInputLength == 0)
21+
{
22+
return DefaultMaxLength;
23+
}
24+
25+
return configuredInputLength.Value;
26+
}
27+
}

0 commit comments

Comments
 (0)