-
Notifications
You must be signed in to change notification settings - Fork 466
Improved metadata binding parsing and validation. #11101
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
bb445bb
Improved metadata binding parsing and validation.
kshyju 432e545
Merge branch 'dev' into shkr/placeholder_start
kshyju 25e9506
Adding another test input to check `:` in binding value.
kshyju dff555b
Sanitize only subset of bindings.
kshyju b913ac2
Improvements.
kshyju fdb2649
empty string/null tests.
kshyju 2bb8812
Switching from Parse to Load and setting DateParseHandling to None by…
kshyju 3377dd6
Test improvements.
kshyju 41c9e43
Merge branch 'dev' into shkr/placeholder_start
kshyju 026acf8
Linting fixes.
kshyju 4d511d5
PR feedback updates.
kshyju c09ebd2
Merge branch 'dev' into shkr/placeholder_start
kshyju File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,12 @@ | ||
### Release notes | ||
|
||
<!-- Please add your release notes in the following format: | ||
- My change description (#PR) | ||
--> | ||
- Adding activity sources for Durable and WebJobs (Kafka and RabbitMQ) (#11137) | ||
- Add JitTrace Files for v4.1041 | ||
- Fix startup deadlock on transient exceptions (#11142) | ||
- Add warning log for end of support bundle version, any bundle version < 4 (#11075), (#11160) | ||
- Handles loading extensions.json with empty extensions(#11174) | ||
- Update HttpWorkerOptions to implement IOptionsFormatter (#11175) | ||
### Release notes | ||
|
||
<!-- Please add your release notes in the following format: | ||
- My change description (#PR) | ||
--> | ||
- Adding activity sources for Durable and WebJobs (Kafka and RabbitMQ) (#11137) | ||
- Add JitTrace Files for v4.1041 | ||
- Fix startup deadlock on transient exceptions (#11142) | ||
- Add warning log for end of support bundle version, any bundle version < 4 (#11075), (#11160) | ||
- Handles loading extensions.json with empty extensions(#11174) | ||
- Update HttpWorkerOptions to implement IOptionsFormatter (#11175) | ||
- Improved metadata binding validation (#11101) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Immutable; | ||
using System.IO; | ||
using System.Linq; | ||
using Microsoft.Azure.WebJobs.Logging; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script | ||
{ | ||
internal static class MetadataJsonHelper | ||
{ | ||
/// <summary> | ||
/// Sanitizes the values of top-level properties in the specified <see cref="JObject"/> | ||
/// whose names match any in the provided collection, using case-insensitive comparison. | ||
/// The original property casing is preserved. | ||
/// <strong>Note:</strong> This method mutates the input <see cref="JObject"/> only if one or more property values are sanitized. | ||
/// </summary> | ||
/// <param name="jsonObject">The <see cref="JObject"/> to sanitize. This object may be modified in place.</param> | ||
/// <param name="propertyNames">A collection of top-level property names to sanitize.</param> | ||
/// <returns> | ||
/// The modified <see cref="JObject"/> with the specified properties' values sanitized if found. | ||
/// </returns> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown if <paramref name="jsonObject"/> or <paramref name="propertyNames"/> is <c>null</c>. | ||
/// </exception> | ||
public static JObject SanitizeProperties(JObject jsonObject, ImmutableHashSet<string> propertyNames) | ||
{ | ||
ArgumentNullException.ThrowIfNull(jsonObject, nameof(jsonObject)); | ||
ArgumentNullException.ThrowIfNull(propertyNames, nameof(propertyNames)); | ||
surgupta-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (propertyNames.Count == 0) | ||
{ | ||
return jsonObject; | ||
} | ||
|
||
foreach (var prop in jsonObject.Properties()) | ||
{ | ||
if (propertyNames.Contains(prop.Name, StringComparer.OrdinalIgnoreCase)) | ||
kshyju marked this conversation as resolved.
Show resolved
Hide resolved
kshyju marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (prop.Value.Type == JTokenType.Null) | ||
{ | ||
continue; | ||
} | ||
|
||
var valueToSanitize = prop.Value.Type == JTokenType.String ? (string)prop.Value : prop.Value.ToString(); | ||
jsonObject[prop.Name] = Sanitizer.Sanitize(valueToSanitize); | ||
} | ||
} | ||
|
||
return jsonObject; | ||
kshyju marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/// <summary> | ||
/// Parses the input JSON string into a <see cref="JObject"/> and sanitizes the values of top-level properties | ||
/// whose names match any in the provided collection, using case-insensitive comparison. | ||
/// The original property casing is preserved. Allows customization of JSON date parsing behavior. | ||
/// </summary> | ||
/// <param name="json">The JSON string to parse and sanitize.</param> | ||
/// <param name="propertyNames">A collection of top-level property names to sanitize. Case-insensitive matching is used.</param> | ||
/// <param name="dateParseHandling"> | ||
/// Specifies how date strings should be parsed. Defaults to <see cref="DateParseHandling.None"/> to avoid automatic date conversion. | ||
/// </param> | ||
/// <returns> | ||
/// A <see cref="JObject"/> representing the parsed JSON, with the specified properties' values sanitized if found. | ||
/// </returns> | ||
/// <exception cref="ArgumentException"> | ||
/// Thrown if <paramref name="json"/> is <c>null</c>, empty, or whitespace. | ||
/// </exception> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown if <paramref name="propertyNames"/> is <c>null</c>. | ||
/// </exception> | ||
/// <exception cref="JsonReaderException"> | ||
/// Thrown if <paramref name="json"/> is not a valid JSON string. | ||
/// </exception> | ||
public static JObject CreateJObjectWithSanitizedPropertyValue(string json, ImmutableHashSet<string> propertyNames, DateParseHandling dateParseHandling = DateParseHandling.None) | ||
{ | ||
if (string.IsNullOrWhiteSpace(json)) | ||
{ | ||
throw new ArgumentException("Input JSON cannot be null or empty.", nameof(json)); | ||
} | ||
|
||
ArgumentNullException.ThrowIfNull(propertyNames, nameof(propertyNames)); | ||
|
||
using var stringReader = new StringReader(json); | ||
using var jsonReader = new JsonTextReader(stringReader) | ||
{ | ||
DateParseHandling = dateParseHandling | ||
}; | ||
|
||
var jsonObject = JObject.Load(jsonReader); | ||
|
||
return SanitizeProperties(jsonObject, propertyNames); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Immutable; | ||
using Newtonsoft.Json.Linq; | ||
using Xunit; | ||
|
||
namespace Microsoft.Azure.WebJobs.Script.Tests | ||
{ | ||
public sealed class MetadataJsonHelperTests | ||
{ | ||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_NullJsonObject_ThrowsArgumentNullException() | ||
{ | ||
JObject jsonObject = null; | ||
ImmutableHashSet<string> propertyNames = ["sensitiveProperty"]; | ||
|
||
Assert.Throws<ArgumentNullException>(() => MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames)); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_NullPropertyNames_ThrowsArgumentNullException() | ||
{ | ||
var jsonObject = new JObject(); | ||
ImmutableHashSet<string> propertyNames = null; | ||
|
||
Assert.Throws<ArgumentNullException>(() => MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames)); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_ValidInput_SanitizesMatchingProperties() | ||
{ | ||
var jsonObject = new JObject | ||
{ | ||
{ "sensitiveProperty1", "AccountKey=foo" }, | ||
{ "sensitiveProperty2", "MyConnection" }, | ||
{ "SENSITIVEPROPERTY3", "AccountKey=bar" }, | ||
{ "otherProperty", "value2" } | ||
}; | ||
var sensitiveBindingPropertyNames = ImmutableHashSet.Create("sensitiveProperty1", "sensitiveproperty2", "sensitiveproperty3"); | ||
|
||
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, sensitiveBindingPropertyNames); | ||
|
||
Assert.Equal("[Hidden Credential]", result["sensitiveProperty1"].ToString()); | ||
Assert.Equal("MyConnection", result["sensitiveProperty2"].ToString()); | ||
Assert.Equal("[Hidden Credential]", result["SENSITIVEPROPERTY3"].ToString()); | ||
Assert.Equal("value2", result["otherProperty"].ToString()); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_NoMatchingProperties_DoesNotSanitize() | ||
{ | ||
var jsonObject = new JObject | ||
{ | ||
{ "otherProperty1", "value1" }, | ||
{ "otherProperty2", "value2" }, | ||
{ "otherProperty3", "AccountKey=foo" } | ||
}; | ||
var sensitiveBindingPropertyNames = ImmutableHashSet.Create("sensitiveProperty"); | ||
|
||
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, sensitiveBindingPropertyNames); | ||
|
||
Assert.Equal("value1", result["otherProperty1"].ToString()); | ||
Assert.Equal("value2", result["otherProperty2"].ToString()); | ||
Assert.Equal("AccountKey=foo", result["otherProperty3"].ToString()); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_StringInput_NullOrEmptyJson_ThrowsArgumentException() | ||
{ | ||
string json = null; | ||
var propertyNames = ImmutableHashSet.Create("sensitiveProperty"); | ||
|
||
Assert.Throws<ArgumentException>(() => MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames)); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_StringInput_InvalidJson_ThrowsJsonReaderException() | ||
{ | ||
var json = "invalid json"; | ||
var propertyNames = ImmutableHashSet.Create("sensitiveProperty"); | ||
|
||
Assert.Throws<Newtonsoft.Json.JsonReaderException>(() => MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames)); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_StringInput_ValidJson_SanitizesMatchingProperties() | ||
{ | ||
var json = """{ "SensitiveProperty": "pwd=12345", "otherProperty": "value2" }"""; | ||
var propertyNames = ImmutableHashSet.Create("sensitiveproperty"); | ||
|
||
var result = MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames); | ||
|
||
Assert.Equal("[Hidden Credential]", result["SensitiveProperty"].ToString()); | ||
Assert.Equal("value2", result["otherProperty"].ToString()); | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_NullSensitiveProperty_DoesNotThrow() | ||
{ | ||
var jsonObject = new JObject | ||
{ | ||
{ "connection", null }, | ||
{ "otherProperty1", "value1" }, | ||
{ "otherProperty2", string.Empty } | ||
}; | ||
var propertyNames = ImmutableHashSet.Create("connection", "otherProperty2"); | ||
|
||
var result = MetadataJsonHelper.SanitizeProperties(jsonObject, propertyNames); | ||
|
||
Assert.Equal(JTokenType.Null, result["connection"].Type); // Ensure null remains null | ||
Assert.Equal("value1", result["otherProperty1"].ToString()); | ||
Assert.Equal(string.Empty, result["otherProperty2"].ToString()); // Ensure empty string remains empty | ||
} | ||
|
||
[Fact] | ||
public void CreateJObjectWithSanitizedPropertyValue_StringInput_DateTimeWithTimezoneOffset_RemainsUnchanged() | ||
{ | ||
var json = """{ "timestamp": "2025-07-03T12:30:45+02:00", "otherProperty": "value2" }"""; | ||
var propertyNames = ImmutableHashSet.Create("sensitiveProperty"); | ||
|
||
var result = MetadataJsonHelper.CreateJObjectWithSanitizedPropertyValue(json, propertyNames); | ||
|
||
Assert.Equal("2025-07-03T12:30:45+02:00", result["timestamp"].ToObject<string>()); // ensure the value remains unchanged(not parsed as DateTime) | ||
Assert.Equal("value2", result["otherProperty"].ToString()); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.