Skip to content

Commit

Permalink
Add validation for null props inside objects inside arrays. (#4896) (… (
Browse files Browse the repository at this point in the history
#5034)

…#4941)

Since IoT Hub does not support patch operations on arrays (if you want to update array, you need to replace it), they don't allow null values or null properties inside arrays. EdgeHub, on the other hand, allows that. So, validation can pass on the EdgeHub side, but the update will be rejected by the hub. This situation allows for sending bad payloads, that blocks any further twin updates unless bad property is removed/cleared.
  • Loading branch information
vipeller committed May 26, 2021
1 parent 0409608 commit a6d7fee
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,40 @@ public class ReportedPropertiesValidator : IValidator<TwinCollection>
public void Validate(TwinCollection reportedProperties)
{
Preconditions.CheckNotNull(reportedProperties, nameof(reportedProperties));

JToken reportedPropertiesJToken = JToken.Parse(reportedProperties.ToJson());
ValidateTwinProperties(reportedPropertiesJToken, 1);
ValidateTwinCollectionSize(reportedProperties);
// root level has no property name.
ValidateToken(string.Empty, reportedPropertiesJToken, 0, false);
}

static void ValidateTwinProperties(JToken properties, int currentDepth)
static void ValidateToken(string name, JToken item, int currentDepth, bool inArray)
{
foreach (JProperty kvp in ((JObject)properties).Properties())
{
ValidatePropertyNameAndLength(kvp.Name);
ValidatePropertyNameAndLength(name);

ValidateValueType(kvp.Name, kvp.Value);
if (item is JObject @object)
{
ValidateTwinDepth(currentDepth);

if (kvp.Value is JValue)
// do validation recursively
foreach (JProperty kvp in @object.Properties())
{
if (kvp.Value.Type is JTokenType.Integer)
{
ValidateIntegerValue(kvp.Name, (long)kvp.Value);
}
else
{
string s = kvp.Value.ToString();
ValidatePropertyValueLength(kvp.Name, s);
}
ValidateToken(kvp.Name, kvp.Value, currentDepth + 1, inArray);
}
}

if (kvp.Value != null && kvp.Value is JObject)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}
if (item is JValue value)
{
ValidateValueType(name, value);
ValidateValue(name, value, inArray);
}

// do validation recursively
ValidateTwinProperties(kvp.Value, currentDepth + 1);
}
if (item is JArray array)
{
ValidateTwinDepth(currentDepth);

// do array validation
ValidateArrayContent(array, currentDepth + 1);
}
}

Expand Down Expand Up @@ -94,6 +92,51 @@ static void ValidatePropertyValueLength(string name, string value)
}
}

static void ValidateArrayContent(JArray array, int currentDepth)
{
foreach (var item in array)
{
if (item.Type is JTokenType.Null)
{
throw new InvalidOperationException("Arrays cannot contain 'null' as value");
}

if (item is JArray inner)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}

// do array validation
ValidateArrayContent(inner, currentDepth + 1);
}
else
{
// items in the array don't have property name.
ValidateToken(string.Empty, item, currentDepth, true);
}
}
}

static void ValidateValue(string name, JValue value, bool inArray)
{
if (inArray && value.Type is JTokenType.Null)
{
throw new InvalidOperationException($"Property {name} of an object in an array cannot be 'null'");
}

if (value.Type is JTokenType.Integer)
{
ValidateIntegerValue(name, (long)value);
}
else
{
string s = value.ToString();
ValidatePropertyValueLength(name, s);
}
}

[AssertionMethod]
static void ValidateIntegerValue(string name, long value)
{
Expand All @@ -119,5 +162,13 @@ static void ValidateTwinCollectionSize(TwinCollection collection)
throw new InvalidOperationException($"Twin properties size {size} exceeds maximum {TwinPropertyDocMaxLength}");
}
}

static void ValidateTwinDepth(int currentDepth)
{
if (currentDepth > TwinPropertyMaxDepth)
{
throw new InvalidOperationException($"Nested depth of twin property exceeds {TwinPropertyMaxDepth}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,57 @@ public static IEnumerable<object[]> GetTwinCollections()
"Nested depth of twin property exceeds 10"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
level1 = new
{
// level 2
array1 = new[]
{
// level 3
new[]
{
// level 4
new[]
{
// level 5
new[]
{
// level 6
new[]
{
// level 7
new[]
{
// level 8
new[]
{
// level 9
new[]
{
// level 10
new[]
{
// level 11
new[] { "one", "two", "three" },
}
}
}
}
}
}
}
},
}
}
})),
typeof(InvalidOperationException),
"Nested depth of twin property exceeds 10"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
Expand Down Expand Up @@ -197,6 +248,106 @@ public static IEnumerable<object[]> GetTwinCollections()
yield return new object[]
{
new TwinCollection("{ \"ok\": [\"good\"], \"ok2\": [], \"level1\": [{ \"field1\": null }] }"),
typeof(InvalidOperationException),
"Property field1 of an object in an array cannot be 'null'"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
complex = new
{
array1 = new object[]
{
"one",
"two",
new
{
array2 = new[]
{
new { hello = (string)null }
}
},
}
}
})),
typeof(InvalidOperationException),
"Property hello of an object in an array cannot be 'null'"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
array = new string[] { "foo", null, "boo" }
})),
typeof(InvalidOperationException),
"Arrays cannot contain 'null' as value"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
complex = new
{
ok = "ok",
pi = 3.14,
sometime = new DateTime(2021, 1, 20),
array = new[]
{
"one",
"two",
null,
"four",
}
}
})),
typeof(InvalidOperationException),
"Arrays cannot contain 'null' as value"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
complex = new
{
ok = "ok",
array = new[]
{
new[] { "one", "two", "three" },
new[] { "four", null, "six" },
}
}
})),
typeof(InvalidOperationException),
"Arrays cannot contain 'null' as value"
};

yield return new object[]
{
new TwinCollection(JsonConvert.SerializeObject(new
{
ok = "ok",
complex = new
{
ok = "ok",
array = new[]
{
new[] { "one", "two", "three" },
new[] { "four", "five", "six" },
new object[] { "seven", new { ok = "ok" } },
},
pi = 3.14,
sometime = new DateTime(2021, 1, 20),
}
})),
null,
string.Empty
};
Expand Down

0 comments on commit a6d7fee

Please sign in to comment.