From 3f8407b361bed8a808ee9863d33952dae67603c0 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Mon, 18 Nov 2024 17:42:57 -0800 Subject: [PATCH] Harden parsing of [Range] attribute values --- .../Extensions/JsonNodeSchemaExtensions.cs | 19 +++++- .../OpenApiSchemaService.ParameterSchemas.cs | 60 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index 9cc97724b1b3..7e456214cfb9 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -91,8 +91,23 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable } else if (attribute is RangeAttribute rangeAttribute) { - schema[OpenApiSchemaKeywords.MinimumKeyword] = decimal.Parse(rangeAttribute.Minimum.ToString()!, CultureInfo.InvariantCulture); - schema[OpenApiSchemaKeywords.MaximumKeyword] = decimal.Parse(rangeAttribute.Maximum.ToString()!, CultureInfo.InvariantCulture); + // Use InvariantCulture if explicitly requested or if the range has been set via the + // RangeAttribute(double, double) or RangeAttribute(int, int) constructors. + var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture || rangeAttribute.Minimum is double || rangeAttribute.Maximum is int + ? CultureInfo.InvariantCulture + : CultureInfo.CurrentCulture; + + var minString = rangeAttribute.Minimum.ToString(); + var maxString = rangeAttribute.Maximum.ToString(); + + if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var minDecimal)) + { + schema[OpenApiSchemaKeywords.MinimumKeyword] = minDecimal; + } + if (decimal.TryParse(maxString, NumberStyles.Any, targetCulture, out var maxDecimal)) + { + schema[OpenApiSchemaKeywords.MaximumKeyword] = maxDecimal; + } } else if (attribute is RegularExpressionAttribute regularExpressionAttribute) { diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs index 96e3b3ccbe15..1c7cb1ba5746 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs @@ -3,10 +3,12 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; @@ -338,6 +340,7 @@ await VerifyOpenApiDocument(action, document => [([MinLength(2)] int[] id) => {}, (OpenApiSchema schema) => Assert.Equal(2, schema.MinItems)], [([Length(4, 8)] int[] id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.MinItems); Assert.Equal(8, schema.MaxItems); }], [([Range(4, 8)]int id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.Minimum); Assert.Equal(8, schema.Maximum); }], + [([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }], [([StringLength(10)] string name) => {}, (OpenApiSchema schema) => { Assert.Equal(10, schema.MaxLength); Assert.Equal(0, schema.MinLength); }], [([StringLength(10, MinimumLength = 5)] string name) => {}, (OpenApiSchema schema) => { Assert.Equal(10, schema.MaxLength); Assert.Equal(5, schema.MinLength); }], [([Url] string url) => {}, (OpenApiSchema schema) => { Assert.Equal("string", schema.Type); Assert.Equal("uri", schema.Format); }], @@ -365,6 +368,63 @@ await VerifyOpenApiDocument(builder, document => }); } + public static object[][] RouteParametersWithRangeAttributes => + [ + [([Range(4, 8)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.Minimum); Assert.Equal(8, schema.Maximum); }], + [([Range(int.MinValue, int.MaxValue)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(int.MinValue, schema.Minimum); Assert.Equal(int.MaxValue, schema.Maximum); }], + [([Range(0, double.MaxValue)] double id) => {}, (OpenApiSchema schema) => { Assert.Equal(0, schema.Minimum); Assert.Null(schema.Maximum); }], + [([Range(typeof(double), "0", "1.79769313486232E+308")] double id) => {}, (OpenApiSchema schema) => { Assert.Equal(0, schema.Minimum); Assert.Null(schema.Maximum); }], + [([Range(typeof(long), "-9223372036854775808", "9223372036854775807")] long id) => {}, (OpenApiSchema schema) => { Assert.Equal(long.MinValue, schema.Minimum); Assert.Equal(long.MaxValue, schema.Maximum); }], + [([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }], + ]; + + [Theory] + [MemberData(nameof(RouteParametersWithRangeAttributes))] + public async Task GetOpenApiParameters_HandlesRouteParametersWithRangeAttributes(Delegate requestHandler, Action verifySchema) + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api/{id}", requestHandler); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api/{id}"].Operations[OperationType.Get]; + var parameter = Assert.Single(operation.Parameters); + verifySchema(parameter.Schema); + }); + } + + public static object[][] RouteParametersWithRangeAttributes_CultureInfo => + [ + [([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }], + [([Range(typeof(decimal), "1,99", "3,99")] decimal id) => {}, (OpenApiSchema schema) => { Assert.Equal(1.99m, schema.Minimum); Assert.Equal(3.99m, schema.Maximum); }], + [([Range(typeof(decimal), "1,99", "3,99", ParseLimitsInInvariantCulture = true)] decimal id) => {}, (OpenApiSchema schema) => { Assert.Equal(199, schema.Minimum); Assert.Equal(399, schema.Maximum); }], + [([Range(1000, 2000)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(1000, schema.Minimum); Assert.Equal(2000, schema.Maximum); }] + ]; + + [Theory] + [MemberData(nameof(RouteParametersWithRangeAttributes_CultureInfo))] + [UseCulture("fr-FR")] + public async Task GetOpenApiParameters_HandlesRouteParametersWithRangeAttributes_CultureInfo(Delegate requestHandler, Action verifySchema) + { + // Arrange + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api/{id}", requestHandler); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api/{id}"].Operations[OperationType.Get]; + var parameter = Assert.Single(operation.Parameters); + verifySchema(parameter.Schema); + }); + } + [Fact] public async Task GetOpenApiParameters_HandlesParametersWithRequiredAttribute() {