Skip to content

Commit 6500504

Browse files
committed
Merge branch 'dev' into jviau/eng/packages-props
2 parents 7686611 + 48712e8 commit 6500504

File tree

6 files changed

+245
-13
lines changed

6 files changed

+245
-13
lines changed

NuGet.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
<add key="AzureFunctionsRelease" value="https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/AzureFunctionsRelease/nuget/v3/index.json" />
99
<add key="AzureFunctionsPreRelease" value="https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/AzureFunctionsPreRelease/nuget/v3/index.json" />
1010
<add key="AzureFunctionsTempStaging" value="https://azfunc.pkgs.visualstudio.com/e6a70c92-4128-439f-8012-382fe78d6396/_packaging/AzureFunctionsTempStaging/nuget/v3/index.json" />
11+
<add key="AzureFunctionsInfra" value="https://pkgs.dev.azure.com/azfunc/public/_packaging/infra/nuget/v3/index.json" />
1112
</packageSources>
1213
</configuration>

eng/ci/templates/official/jobs/process-coldstart.yml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ jobs:
3737
artifact: $(artifactName)
3838

3939
steps:
40-
- checkout: none
4140
- pwsh: |
4241
$sampleSize = $env:SAMPLE_SIZE
4342
$values = @()
@@ -98,15 +97,8 @@ jobs:
9897
scriptType: 'pscore'
9998
scriptLocation: 'inlineScript'
10099
inlineScript: |
101-
$json = Get-Content -Path $(resultsJsonPath) -Raw
102-
$dateTimeUtc = [datetimeoffset]::UtcNow
103-
104-
$query = @"
105-
INSERT INTO ColdStart (DateTimeUtc, OS, Description, Document)
106-
VALUES ('$dateTimeUtc', '${{ parameters.os }}', '${{ parameters.description }}', '$json')
107-
"@
108-
109-
Invoke-Sqlcmd -ConnectionString "$(ColdStartResultsSqlConnectionString)" -Query $query
110-
100+
$env:PATH = [System.IO.Path]::Combine($env:USERPROFILE, ".dotnet", "tools") + ";$env:PATH"
101+
dotnet tool install -g Microsoft.Azure.Functions.ColdStartDataWriter --prerelease
102+
func-cold-start-data-writer --os '${{ parameters.os }}' --description '${{ parameters.description }}' --sql-connection '$(ColdStartResultsSqlConnectionString)' --document-file-path '$(resultsJsonPath)'
111103
displayName: Persist results
112104
condition: eq(variables['Build.Reason'], 'Schedule')

release_notes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@
88
- Fix startup deadlock on transient exceptions (#11142)
99
- Add warning log for end of support bundle version, any bundle version < 4 (#11075), (#11160)
1010
- Handles loading extensions.json with empty extensions(#11174)
11-
- Update HttpWorkerOptions to implement IOptionsFormatter (#11175)
11+
- Update HttpWorkerOptions to implement IOptionsFormatter (#11175)
12+
- Improved metadata binding validation (#11101)
13+
- Skip logging errors on gRPC client disconnect (#10572)

src/WebJobs.Script.Grpc/Server/FunctionRpcService.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.IO;
56
using System.Reactive.Linq;
67
using System.Threading;
78
using System.Threading.Channels;
89
using System.Threading.Tasks;
910
using Grpc.Core;
11+
using Microsoft.AspNetCore.Connections;
1012
using Microsoft.Azure.WebJobs.Script.Eventing;
1113
using Microsoft.Azure.WebJobs.Script.Grpc.Eventing;
1214
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
1315
using Microsoft.Extensions.Logging;
14-
1516
using MsgType = Microsoft.Azure.WebJobs.Script.Grpc.Messages.StreamingMessage.ContentOneofCase;
1617

1718
namespace Microsoft.Azure.WebJobs.Script.Grpc
@@ -75,6 +76,14 @@ static Task<Task<bool>> MoveNextAsync(IAsyncStreamReader<StreamingMessage> reque
7576
}
7677
}
7778
}
79+
catch (IOException ex) when (ex.InnerException is ConnectionAbortedException && context.CancellationToken.IsCancellationRequested)
80+
{
81+
// Expected when the client disconnects.
82+
// Client side stream termination (e.g., process exit or network interruption)
83+
// can cause MoveNext() to throw an IOException with a ConnectionAbortedException as the inner exception.
84+
// If ServerCallContext's cancellation token is also canceled at this point, the exception can be safely ignored.
85+
return;
86+
}
7887
catch (Exception rpcException)
7988
{
8089
// We catch the exception, just to report it, then re-throw it
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using System.IO;
7+
using System.Linq;
8+
using Microsoft.Azure.WebJobs.Logging;
9+
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
11+
12+
namespace Microsoft.Azure.WebJobs.Script
13+
{
14+
internal static class MetadataJsonHelper
15+
{
16+
/// <summary>
17+
/// Sanitizes the values of top-level properties in the specified <see cref="JObject"/>
18+
/// whose names match any in the provided collection, using case-insensitive comparison.
19+
/// The original property casing is preserved.
20+
/// <strong>Note:</strong> This method mutates the input <see cref="JObject"/> only if one or more property values are sanitized.
21+
/// </summary>
22+
/// <param name="jsonObject">The <see cref="JObject"/> to sanitize. This object may be modified in place.</param>
23+
/// <param name="propertyNames">A collection of top-level property names to sanitize.</param>
24+
/// <returns>
25+
/// The modified <see cref="JObject"/> with the specified properties' values sanitized if found.
26+
/// </returns>
27+
/// <exception cref="ArgumentNullException">
28+
/// Thrown if <paramref name="jsonObject"/> or <paramref name="propertyNames"/> is <c>null</c>.
29+
/// </exception>
30+
public static JObject SanitizeProperties(JObject jsonObject, ImmutableHashSet<string> propertyNames)
31+
{
32+
ArgumentNullException.ThrowIfNull(jsonObject, nameof(jsonObject));
33+
ArgumentNullException.ThrowIfNull(propertyNames, nameof(propertyNames));
34+
35+
if (propertyNames.Count == 0)
36+
{
37+
return jsonObject;
38+
}
39+
40+
foreach (var prop in jsonObject.Properties())
41+
{
42+
if (propertyNames.Contains(prop.Name, StringComparer.OrdinalIgnoreCase))
43+
{
44+
if (prop.Value.Type == JTokenType.Null)
45+
{
46+
continue;
47+
}
48+
49+
var valueToSanitize = prop.Value.Type == JTokenType.String ? (string)prop.Value : prop.Value.ToString();
50+
jsonObject[prop.Name] = Sanitizer.Sanitize(valueToSanitize);
51+
}
52+
}
53+
54+
return jsonObject;
55+
}
56+
57+
/// <summary>
58+
/// Parses the input JSON string into a <see cref="JObject"/> and sanitizes the values of top-level properties
59+
/// whose names match any in the provided collection, using case-insensitive comparison.
60+
/// The original property casing is preserved. Allows customization of JSON date parsing behavior.
61+
/// </summary>
62+
/// <param name="json">The JSON string to parse and sanitize.</param>
63+
/// <param name="propertyNames">A collection of top-level property names to sanitize. Case-insensitive matching is used.</param>
64+
/// <param name="dateParseHandling">
65+
/// Specifies how date strings should be parsed. Defaults to <see cref="DateParseHandling.None"/> to avoid automatic date conversion.
66+
/// </param>
67+
/// <returns>
68+
/// A <see cref="JObject"/> representing the parsed JSON, with the specified properties' values sanitized if found.
69+
/// </returns>
70+
/// <exception cref="ArgumentException">
71+
/// Thrown if <paramref name="json"/> is <c>null</c>, empty, or whitespace.
72+
/// </exception>
73+
/// <exception cref="ArgumentNullException">
74+
/// Thrown if <paramref name="propertyNames"/> is <c>null</c>.
75+
/// </exception>
76+
/// <exception cref="JsonReaderException">
77+
/// Thrown if <paramref name="json"/> is not a valid JSON string.
78+
/// </exception>
79+
public static JObject CreateJObjectWithSanitizedPropertyValue(string json, ImmutableHashSet<string> propertyNames, DateParseHandling dateParseHandling = DateParseHandling.None)
80+
{
81+
if (string.IsNullOrWhiteSpace(json))
82+
{
83+
throw new ArgumentException("Input JSON cannot be null or empty.", nameof(json));
84+
}
85+
86+
ArgumentNullException.ThrowIfNull(propertyNames, nameof(propertyNames));
87+
88+
using var stringReader = new StringReader(json);
89+
using var jsonReader = new JsonTextReader(stringReader)
90+
{
91+
DateParseHandling = dateParseHandling
92+
};
93+
94+
var jsonObject = JObject.Load(jsonReader);
95+
96+
return SanitizeProperties(jsonObject, propertyNames);
97+
}
98+
}
99+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Immutable;
6+
using Newtonsoft.Json.Linq;
7+
using Xunit;
8+
9+
namespace Microsoft.Azure.WebJobs.Script.Tests
10+
{
11+
public sealed class MetadataJsonHelperTests
12+
{
13+
[Fact]
14+
public void CreateJObjectWithSanitizedPropertyValue_NullJsonObject_ThrowsArgumentNullException()
15+
{
16+
JObject jsonObject = null;
17+
ImmutableHashSet<string> propertyNames = ["sensitiveProperty"];
18+
19+
Assert.Throws<ArgumentNullException>(() => MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames));
20+
}
21+
22+
[Fact]
23+
public void CreateJObjectWithSanitizedPropertyValue_NullPropertyNames_ThrowsArgumentNullException()
24+
{
25+
var jsonObject = new JObject();
26+
ImmutableHashSet<string> propertyNames = null;
27+
28+
Assert.Throws<ArgumentNullException>(() => MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames));
29+
}
30+
31+
[Fact]
32+
public void CreateJObjectWithSanitizedPropertyValue_ValidInput_SanitizesMatchingProperties()
33+
{
34+
var jsonObject = new JObject
35+
{
36+
{ "sensitiveProperty1", "AccountKey=foo" },
37+
{ "sensitiveProperty2", "MyConnection" },
38+
{ "SENSITIVEPROPERTY3", "AccountKey=bar" },
39+
{ "otherProperty", "value2" }
40+
};
41+
var sensitiveBindingPropertyNames = ImmutableHashSet.Create("sensitiveProperty1", "sensitiveproperty2", "sensitiveproperty3");
42+
43+
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, sensitiveBindingPropertyNames);
44+
45+
Assert.Equal("[Hidden Credential]", result["sensitiveProperty1"].ToString());
46+
Assert.Equal("MyConnection", result["sensitiveProperty2"].ToString());
47+
Assert.Equal("[Hidden Credential]", result["SENSITIVEPROPERTY3"].ToString());
48+
Assert.Equal("value2", result["otherProperty"].ToString());
49+
}
50+
51+
[Fact]
52+
public void CreateJObjectWithSanitizedPropertyValue_NoMatchingProperties_DoesNotSanitize()
53+
{
54+
var jsonObject = new JObject
55+
{
56+
{ "otherProperty1", "value1" },
57+
{ "otherProperty2", "value2" },
58+
{ "otherProperty3", "AccountKey=foo" }
59+
};
60+
var sensitiveBindingPropertyNames = ImmutableHashSet.Create("sensitiveProperty");
61+
62+
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, sensitiveBindingPropertyNames);
63+
64+
Assert.Equal("value1", result["otherProperty1"].ToString());
65+
Assert.Equal("value2", result["otherProperty2"].ToString());
66+
Assert.Equal("AccountKey=foo", result["otherProperty3"].ToString());
67+
}
68+
69+
[Fact]
70+
public void CreateJObjectWithSanitizedPropertyValue_StringInput_NullOrEmptyJson_ThrowsArgumentException()
71+
{
72+
string json = null;
73+
var propertyNames = ImmutableHashSet.Create("sensitiveProperty");
74+
75+
Assert.Throws<ArgumentException>(() => MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames));
76+
}
77+
78+
[Fact]
79+
public void CreateJObjectWithSanitizedPropertyValue_StringInput_InvalidJson_ThrowsJsonReaderException()
80+
{
81+
var json = "invalid json";
82+
var propertyNames = ImmutableHashSet.Create("sensitiveProperty");
83+
84+
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() => MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames));
85+
}
86+
87+
[Fact]
88+
public void CreateJObjectWithSanitizedPropertyValue_StringInput_ValidJson_SanitizesMatchingProperties()
89+
{
90+
var json = """{ "SensitiveProperty": "pwd=12345", "otherProperty": "value2" }""";
91+
var propertyNames = ImmutableHashSet.Create("sensitiveproperty");
92+
93+
var result = MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames);
94+
95+
Assert.Equal("[Hidden Credential]", result["SensitiveProperty"].ToString());
96+
Assert.Equal("value2", result["otherProperty"].ToString());
97+
}
98+
99+
[Fact]
100+
public void CreateJObjectWithSanitizedPropertyValue_NullSensitiveProperty_DoesNotThrow()
101+
{
102+
var jsonObject = new JObject
103+
{
104+
{ "connection", null },
105+
{ "otherProperty1", "value1" },
106+
{ "otherProperty2", string.Empty }
107+
};
108+
var propertyNames = ImmutableHashSet.Create("connection", "otherProperty2");
109+
110+
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames);
111+
112+
Assert.Equal(JTokenType.Null, result["connection"].Type); // Ensure null remains null
113+
Assert.Equal("value1", result["otherProperty1"].ToString());
114+
Assert.Equal(string.Empty, result["otherProperty2"].ToString()); // Ensure empty string remains empty
115+
}
116+
117+
[Fact]
118+
public void CreateJObjectWithSanitizedPropertyValue_StringInput_DateTimeWithTimezoneOffset_RemainsUnchanged()
119+
{
120+
var json = """{ "timestamp": "2025-07-03T12:30:45+02:00", "otherProperty": "value2" }""";
121+
var propertyNames = ImmutableHashSet.Create("sensitiveProperty");
122+
123+
var result = MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames);
124+
125+
Assert.Equal("2025-07-03T12:30:45+02:00", result["timestamp"].ToObject<string>()); // ensure the value remains unchanged(not parsed as DateTime)
126+
Assert.Equal("value2", result["otherProperty"].ToString());
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)