From 298e37851f330e8181fce13e483acdc340c83e33 Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 08:35:22 +0200 Subject: [PATCH 1/6] Added logic to stop JsonPatch from using accessing specific properties --- SystemTextJsonPatch/DenyPatchAttribute.cs | 9 +++++++ .../JsonPatchAccessDeniedException.cs | 24 ++++++++++++++++++ .../Internal/PropertyProxyCache.cs | 25 +++++++++++++------ 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 SystemTextJsonPatch/DenyPatchAttribute.cs create mode 100644 SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs diff --git a/SystemTextJsonPatch/DenyPatchAttribute.cs b/SystemTextJsonPatch/DenyPatchAttribute.cs new file mode 100644 index 0000000..769d27d --- /dev/null +++ b/SystemTextJsonPatch/DenyPatchAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace SystemTextJsonPatch +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class DenyPatchAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs new file mode 100644 index 0000000..68c6480 --- /dev/null +++ b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace SystemTextJsonPatch.Exceptions +{ + +#pragma warning disable CA1032 // Implement standard exception constructors + [Serializable] + public class JsonPatchAccessDeniedException : JsonPatchException +#pragma warning restore CA1032 // Implement standard exception constructors + { + public JsonPatchAccessDeniedException(string message, Exception? innerException) : base(message, innerException) + { + } + + public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this($"Patch is not allowed to access the property {propertyInfo?.Name ?? "N/A"} on the type {propertyInfo?.DeclaringType?.Name ?? "N/A"}", null) + { + + } + } +} + diff --git a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs index 9049624..946dd22 100644 --- a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs +++ b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Reflection; using System.Text.Json.Serialization; +using SystemTextJsonPatch.Exceptions; using SystemTextJsonPatch.Internal.Proxies; namespace SystemTextJsonPatch.Internal @@ -39,21 +40,31 @@ internal static class PropertyProxyCache { var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute(); if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.OrdinalIgnoreCase)) - { - return new PropertyProxy(propertyInfo); - } - } + { + EnsureAccessToProperty(propertyInfo); + return new PropertyProxy(propertyInfo); + } + } // If it didn't find match by JsonPropertyName then use property name foreach (var propertyInfo in properties) { if (string.Equals(propertyInfo.Name, propName, StringComparison.OrdinalIgnoreCase)) - { - return new PropertyProxy(propertyInfo); + { + EnsureAccessToProperty(propertyInfo); + return new PropertyProxy(propertyInfo); } } return null; } - } + + private static void EnsureAccessToProperty(PropertyInfo propertyInfo) + { + if (propertyInfo.GetCustomAttribute(typeof(DenyPatchAttribute), true) != null) + { + throw new JsonPatchAccessDeniedException(propertyInfo); + } + } + } } From 7b2ff473ea1080a122d3f46f9124e3e37a124d04 Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:04:16 +0200 Subject: [PATCH 2/6] Improved JsonPatchAccessDeniedException --- .../Exceptions/JsonPatchAccessDeniedException.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs index 68c6480..3509566 100644 --- a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs +++ b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs @@ -11,11 +11,19 @@ namespace SystemTextJsonPatch.Exceptions public class JsonPatchAccessDeniedException : JsonPatchException #pragma warning restore CA1032 // Implement standard exception constructors { + public string Type { get; } = "N/A"; + public string Property { get; } = "N/A"; public JsonPatchAccessDeniedException(string message, Exception? innerException) : base(message, innerException) { } - public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this($"Patch is not allowed to access the property {propertyInfo?.Name ?? "N/A"} on the type {propertyInfo?.DeclaringType?.Name ?? "N/A"}", null) + public JsonPatchAccessDeniedException(string type, string property) : this($"Patch is not allowed to access the property {property} on the type {type}", (Exception?)null) + { + this.Type = type; + this.Property = property; + } + + public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this(propertyInfo?.DeclaringType?.Name ?? "N/A", propertyInfo?.Name ?? "N/A") { } From a00db29a2c15fdfbe777d9d90be5a0eed6700e31 Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:04:55 +0200 Subject: [PATCH 3/6] Added documentation for DenyPatch --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c29288d..804990d 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ public IHttpActionResult Patch( } ``` +## Deny access to properties +If you need to stop JsonPatch from reading or writing to some properties, +then you can decorate them with `[DenyPatch]`, if a patch occurs that happens to access the property then a `JsonPatchAccessDeniedException` is thrown. + + ## Migration from v1 JsonPatchDocumentConverterFactory no longer needs to be set to JsonSerializerOptions. From 2e4bb611551b59277e662bbae2bfefaf71649ca0 Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:41:11 +0200 Subject: [PATCH 4/6] Added editorconfig file and ensure that we use tabs instead of spaces --- .editorconfig | 2 ++ SystemTextJsonPatch.sln | 1 + SystemTextJsonPatch/DenyPatchAttribute.cs | 8 ++--- .../JsonPatchAccessDeniedException.cs | 36 +++++++++---------- 4 files changed, 25 insertions(+), 22 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..765b22f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.cs] +indent_style = tab \ No newline at end of file diff --git a/SystemTextJsonPatch.sln b/SystemTextJsonPatch.sln index dbb305f..9439aef 100644 --- a/SystemTextJsonPatch.sln +++ b/SystemTextJsonPatch.sln @@ -9,6 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemTextJsonPatch.Tests", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{24A69526-2027-4376-B6DE-E66612C73D16}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig CHANGELOG.md = CHANGELOG.md README.md = README.md EndProjectSection diff --git a/SystemTextJsonPatch/DenyPatchAttribute.cs b/SystemTextJsonPatch/DenyPatchAttribute.cs index 769d27d..2864f99 100644 --- a/SystemTextJsonPatch/DenyPatchAttribute.cs +++ b/SystemTextJsonPatch/DenyPatchAttribute.cs @@ -2,8 +2,8 @@ namespace SystemTextJsonPatch { - [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] - public sealed class DenyPatchAttribute : Attribute - { - } + [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] + public sealed class DenyPatchAttribute : Attribute + { + } } \ No newline at end of file diff --git a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs index 3509566..2895885 100644 --- a/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs +++ b/SystemTextJsonPatch/Exceptions/JsonPatchAccessDeniedException.cs @@ -7,26 +7,26 @@ namespace SystemTextJsonPatch.Exceptions { #pragma warning disable CA1032 // Implement standard exception constructors - [Serializable] - public class JsonPatchAccessDeniedException : JsonPatchException + [Serializable] + public class JsonPatchAccessDeniedException : JsonPatchException #pragma warning restore CA1032 // Implement standard exception constructors - { - public string Type { get; } = "N/A"; - public string Property { get; } = "N/A"; - public JsonPatchAccessDeniedException(string message, Exception? innerException) : base(message, innerException) - { - } + { + public string Type { get; } = "N/A"; + public string Property { get; } = "N/A"; + public JsonPatchAccessDeniedException(string message, Exception? innerException) : base(message, innerException) + { + } - public JsonPatchAccessDeniedException(string type, string property) : this($"Patch is not allowed to access the property {property} on the type {type}", (Exception?)null) - { - this.Type = type; - this.Property = property; - } + public JsonPatchAccessDeniedException(string type, string property) : this($"Patch is not allowed to access the property {property} on the type {type}", (Exception?)null) + { + this.Type = type; + this.Property = property; + } - public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this(propertyInfo?.DeclaringType?.Name ?? "N/A", propertyInfo?.Name ?? "N/A") - { - - } - } + public JsonPatchAccessDeniedException(PropertyInfo propertyInfo) : this(propertyInfo?.DeclaringType?.Name ?? "N/A", propertyInfo?.Name ?? "N/A") + { + + } + } } From 10fe7eef86ed851ded8b0828239a1689851b557c Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:43:20 +0200 Subject: [PATCH 5/6] Added unit tests --- .../IntegrationTests/DenyIntegrationTest.cs | 94 +++++++++++++++++++ .../TestObjectModels/SimpleObjectWithDeny.cs | 28 ++++++ .../SimpleObjectWithNestedObjectAndDeny.cs | 28 ++++++ 3 files changed, 150 insertions(+) create mode 100644 SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs create mode 100644 SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs create mode 100644 SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs diff --git a/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs b/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs new file mode 100644 index 0000000..6a3dcb5 --- /dev/null +++ b/SystemTextJsonPatch.Tests/IntegrationTests/DenyIntegrationTest.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text.Json; +using SystemTextJsonPatch.Exceptions; +using Xunit; + +namespace SystemTextJsonPatch.IntegrationTests; + +public class DenyIntegrationTest +{ + [Fact] + public void TestInList() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny() + { + SimpleObject = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Test(o => o.SimpleObject.IntegerList, 3, 2); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny.SimpleObject), exception.Property); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny), exception.Type); + } + + [Fact] + public void AddToComplextTypeListSpecifyIndex() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny() + { + SimpleObjectList = new List() + { + new SimpleObject + { + StringProperty = "String1" + }, + new SimpleObject + { + StringProperty = "String2" + } + } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add(o => o.SimpleObjectList[0].StringProperty, "ChangedString1"); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny.SimpleObjectList), exception.Property); + Assert.Equal(nameof(SimpleObjectWithNestedObjectAndDeny), exception.Type); + } + + [Fact] + public void RemoveFromList() + { + // Arrange + var targetObject = new SimpleObjectWithDeny() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDocument = new JsonPatchDocument(); + patchDocument.Remove("IntegerList/2"); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithDeny.IntegerList), exception.Property); + Assert.Equal(nameof(SimpleObjectWithDeny), exception.Type); + } + + + [Fact] + public void InnerObjectDeny() + { + // Arrange + var targetObject = new SimpleObjectWithNestedObjectAndDeny(); + + var patchDocument = new JsonPatchDocument(); + patchDocument.Add("Allow/IntegerValue", 1); + + // Act & Assert + var exception = Assert.Throws(() => patchDocument.ApplyTo(targetObject)); + Assert.Equal(nameof(SimpleObjectWithDeny.IntegerValue), exception.Property); + Assert.Equal(nameof(SimpleObjectWithDeny), exception.Type); + } + +} diff --git a/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs new file mode 100644 index 0000000..1194d72 --- /dev/null +++ b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithDeny.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace SystemTextJsonPatch; + +public class SimpleObjectWithDeny +{ + [DenyPatch] + public List IntegerList { get; set; } + [DenyPatch] + public IList IntegerIList { get; set; } + [DenyPatch] + public int IntegerValue { get; set; } + [DenyPatch] + public int AnotherIntegerValue { get; set; } + [DenyPatch] + public string StringProperty { get; set; } + [DenyPatch] + public string AnotherStringProperty { get; set; } + [DenyPatch] + public decimal DecimalValue { get; set; } + [DenyPatch] + public double DoubleValue { get; set; } + [DenyPatch] + public float FloatValue { get; set; } + [DenyPatch] + public Guid GuidValue { get; set; } +} diff --git a/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs new file mode 100644 index 0000000..91c1416 --- /dev/null +++ b/SystemTextJsonPatch.Tests/TestObjectModels/SimpleObjectWithNestedObjectAndDeny.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace SystemTextJsonPatch; + +public class SimpleObjectWithNestedObjectAndDeny +{ + public int IntegerValue { get; set; } + [DenyPatch] + public NestedObject NestedObject { get; set; } + [DenyPatch] + public SimpleObject SimpleObject { get; set; } + [DenyPatch] + public InheritedObject InheritedObject { get; set; } + [DenyPatch] + public List SimpleObjectList { get; set; } + [DenyPatch] + public IList SimpleObjectIList { get; set; } + + public SimpleObjectWithDeny Allow { get; set; } + public SimpleObjectWithNestedObjectAndDeny() + { + NestedObject = new NestedObject(); + SimpleObject = new SimpleObject(); + InheritedObject = new InheritedObject(); + SimpleObjectList = new List(); + Allow = new SimpleObjectWithDeny(); + } +} From cdcdb564ed6ebb83d65e2ac7888a9d45911a534f Mon Sep 17 00:00:00 2001 From: AnderssonPeter <1308309+AnderssonPeter@users.noreply.github.com> Date: Fri, 4 Oct 2024 09:54:26 +0200 Subject: [PATCH 6/6] Change from spaces to tabs in PropertyProxyCache --- .../Internal/PropertyProxyCache.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs index 946dd22..61dbb11 100644 --- a/SystemTextJsonPatch/Internal/PropertyProxyCache.cs +++ b/SystemTextJsonPatch/Internal/PropertyProxyCache.cs @@ -40,31 +40,31 @@ internal static class PropertyProxyCache { var jsonPropertyNameAttr = propertyInfo.GetCustomAttribute(); if (jsonPropertyNameAttr != null && string.Equals(jsonPropertyNameAttr.Name, propName, StringComparison.OrdinalIgnoreCase)) - { - EnsureAccessToProperty(propertyInfo); - return new PropertyProxy(propertyInfo); - } - } + { + EnsureAccessToProperty(propertyInfo); + return new PropertyProxy(propertyInfo); + } + } // If it didn't find match by JsonPropertyName then use property name foreach (var propertyInfo in properties) { if (string.Equals(propertyInfo.Name, propName, StringComparison.OrdinalIgnoreCase)) - { - EnsureAccessToProperty(propertyInfo); - return new PropertyProxy(propertyInfo); + { + EnsureAccessToProperty(propertyInfo); + return new PropertyProxy(propertyInfo); } } return null; } - private static void EnsureAccessToProperty(PropertyInfo propertyInfo) - { - if (propertyInfo.GetCustomAttribute(typeof(DenyPatchAttribute), true) != null) - { - throw new JsonPatchAccessDeniedException(propertyInfo); - } - } - } + private static void EnsureAccessToProperty(PropertyInfo propertyInfo) + { + if (propertyInfo.GetCustomAttribute(typeof(DenyPatchAttribute), true) != null) + { + throw new JsonPatchAccessDeniedException(propertyInfo); + } + } + } }